#[test]
    fn cut_paste_keeps_exact_target_name_so_collisions_reach_review() {
        let mut reserved = HashSet::new();
        let source = Path::new("assets/Props/desk.material.ron");
        let destination =
            paste_destination(Path::new("assets/Archive"), source, true, &mut reserved);

        assert_eq!(
            destination,
            PathBuf::from("assets/Archive/desk.material.ron")
        );
        assert!(reserved.is_empty());
    }

    #[test]
    fn created_content_undo_is_trash_first_and_fingerprint_guarded() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-content-create-undo-{}",
            uuid::Uuid::new_v4()
        ));
        let created = PathBuf::from("assets/Props/New Folder");
        fs::create_dir_all(root.join(&created)).unwrap();
        let preview = content_pipeline::ContentTransactionPreview {
            operations: vec![content_pipeline::ContentOperation::CreateFolder {
                path: created.clone(),
            }],
            affected_asset_ids: Vec::new(),
            reference_rewrites: Vec::new(),
            collisions: Vec::new(),
        };

        let undo = build_content_undo_entry(&root, &preview).unwrap();

        assert!(matches!(
            &undo.action,
            ContentUndoAction::TrashCreated(paths) if paths == std::slice::from_ref(&created)
        ));
        assert_eq!(first_changed_content_undo_source(&root, &undo), None);
        fs::write(root.join(&created).join("external.txt"), b"external").unwrap();
        assert_eq!(
            first_changed_content_undo_source(&root, &undo),
            Some(created)
        );
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn copied_content_undo_targets_only_the_created_destination() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-content-copy-undo-{}",
            uuid::Uuid::new_v4()
        ));
        let source = PathBuf::from("assets/Props/source.txt");
        let destination = PathBuf::from("assets/Props/source_copy.txt");
        fs::create_dir_all(root.join("assets/Props")).unwrap();
        fs::write(root.join(&source), b"source").unwrap();
        fs::write(root.join(&destination), b"source").unwrap();
        let preview = content_pipeline::ContentTransactionPreview {
            operations: vec![content_pipeline::ContentOperation::Copy {
                source: source.clone(),
                destination: destination.clone(),
            }],
            affected_asset_ids: Vec::new(),
            reference_rewrites: Vec::new(),
            collisions: Vec::new(),
        };

        let undo = build_content_undo_entry(&root, &preview).unwrap();

        assert!(matches!(
            &undo.action,
            ContentUndoAction::TrashCreated(paths) if paths == &[destination]
        ));
        assert_eq!(fs::read(root.join(source)).unwrap(), b"source");
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn trash_restore_undo_is_guarded_by_the_complete_batch() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-content-trash-undo-{}",
            uuid::Uuid::new_v4()
        ));
        let batch = PathBuf::from("assets/.trash/test-batch");
        fs::create_dir_all(root.join(&batch).join("assets/Props")).unwrap();
        fs::write(root.join(&batch).join("assets/Props/item.txt"), b"item").unwrap();
        let fingerprint = content_pipeline::tree_fingerprint(&root.join(&batch)).unwrap();
        let undo = ContentUndoEntry {
            label: "Move Item to Trash".into(),
            action: ContentUndoAction::RestoreTrash(batch.clone()),
            guarded_sources: vec![(batch.clone(), fingerprint)],
        };

        assert_eq!(first_changed_content_undo_source(&root, &undo), None);
        fs::write(root.join(&batch).join("assets/Props/item.txt"), b"external").unwrap();
        assert_eq!(first_changed_content_undo_source(&root, &undo), Some(batch));
        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn manual_trash_restore_forgets_only_its_matching_undo_entry() {
        let matching = PathBuf::from("assets/.trash/matching");
        let other = PathBuf::from("assets/.trash/other");
        let mut state = AssetBrowserUiState {
            content_undo: vec![
                ContentUndoEntry {
                    label: "Move A to Trash".into(),
                    action: ContentUndoAction::RestoreTrash(matching.clone()),
                    guarded_sources: Vec::new(),
                },
                ContentUndoEntry {
                    label: "Move B to Trash".into(),
                    action: ContentUndoAction::RestoreTrash(other.clone()),
                    guarded_sources: Vec::new(),
                },
            ],
            ..Default::default()
        };

        forget_restored_trash_undo(&mut state, &matching);

        assert_eq!(state.content_undo.len(), 1);
        assert!(matches!(
            &state.content_undo[0].action,
            ContentUndoAction::RestoreTrash(path) if path == &other
        ));
    }

    #[test]
    fn content_navigation_history_round_trips_like_a_file_manager() {
        let mut world = World::new();
        let mut assets = EditorAssets::default();
        assets.current_folder = "assets".into();
        world.insert_resource(assets);
        world.insert_resource(AssetBrowserUiState::default());

        navigate_content_folder(&mut world, "assets/Props".into());
        navigate_content_folder(&mut world, "assets/Props/Office".into());
        assert_eq!(
            world.resource::<AssetBrowserUiState>().navigation_back,
            ["assets", "assets/Props"]
        );

        navigate_content_back(&mut world);
        assert_eq!(
            world.resource::<EditorAssets>().current_folder,
            "assets/Props"
        );
        navigate_content_back(&mut world);
        assert_eq!(world.resource::<EditorAssets>().current_folder, "assets");
        navigate_content_forward(&mut world);
        assert_eq!(
            world.resource::<EditorAssets>().current_folder,
            "assets/Props"
        );

        navigate_content_folder(&mut world, "assets/Materials".into());
        assert!(
            world
                .resource::<AssetBrowserUiState>()
                .navigation_forward
                .is_empty()
        );
    }

    #[test]
    fn audio_filter_matches_only_audio_clips() {
        let audio = EditorAsset {
            label: "Impact".into(),
            path: Some("assets/audio/impact.ogg".into()),
            folder_path: "assets/audio".into(),
            kind: EditorAssetKind::AudioClip,
        };
        let texture = EditorAsset {
            kind: EditorAssetKind::Texture,
            ..audio.clone()
        };

        assert!(asset_matches_kind_filter(&audio, AssetKindFilter::Audio));
        assert!(!asset_matches_kind_filter(&texture, AssetKindFilter::Audio));
        assert_eq!(kind_filter_label(AssetKindFilter::Audio), "Audio");
    }

    #[test]
    fn audio_details_label_supported_formats() {
        assert_eq!(audio_format_label("assets/audio/music.oga"), "Ogg audio");
        assert_eq!(audio_format_label("assets/audio/voice.spx"), "Speex");
        assert_eq!(audio_format_label("assets/audio/source.WAV"), "WAV");
        assert_eq!(audio_format_label("assets/audio/source.mp3"), "MP3");
        assert_eq!(audio_format_label("assets/audio/source.flac"), "FLAC");
    }

    #[test]
    fn pbr_grouping_builds_standard_and_surface_texture_bindings() {
        let base = PbrTextureDraft {
            path: "assets/Props/desk_diff_2k.jpg".into(),
            target: "desk".into(),
            role: content_pipeline::PbrTextureRole::BaseColor,
            confidence: content_pipeline::PbrMatchConfidence::High,
            included: true,
        };
        let normal = PbrTextureDraft {
            path: "assets/Props/desk_nor_gl_2k.jpg".into(),
            role: content_pipeline::PbrTextureRole::Normal,
            ..base.clone()
        };
        let arm = PbrTextureDraft {
            path: "assets/Props/desk_arm_2k.jpg".into(),
            role: content_pipeline::PbrTextureRole::PackedOrm,
            ..base.clone()
        };
        let asset = material_asset_from_pbr_group("desk", &[&base, &normal, &arm], &[]);
        let mut material = MaterialDesc::default();
        asset.inputs.apply_to_material_desc(&mut material);

        assert_eq!(
            material.base_color_texture.as_deref(),
            Some("assets/Props/desk_diff_2k.jpg")
        );
        assert_eq!(
            material.normal_map_texture.as_deref(),
            Some("assets/Props/desk_nor_gl_2k.jpg")
        );
        assert_eq!(
            material.metallic_roughness_texture.as_deref(),
            Some("assets/Props/desk_arm_2k.jpg")
        );
        assert!(material
            .textures
            .iter()
            .any(|binding| binding.name == "occlusion_texture"));
    }

    #[test]
    fn pbr_group_count_reflects_manual_merge_and_exclusions() {
        let draft = PbrGroupingDraft {
            folder: "assets/Props".into(),
            textures: vec![
                PbrTextureDraft {
                    path: "assets/Props/a.png".into(),
                    target: "shared".into(),
                    role: content_pipeline::PbrTextureRole::BaseColor,
                    confidence: content_pipeline::PbrMatchConfidence::High,
                    included: true,
                },
                PbrTextureDraft {
                    path: "assets/Props/b.png".into(),
                    target: "shared".into(),
                    role: content_pipeline::PbrTextureRole::Normal,
                    confidence: content_pipeline::PbrMatchConfidence::High,
                    included: true,
                },
                PbrTextureDraft {
                    path: "assets/Props/c.png".into(),
                    target: "ignored".into(),
                    role: content_pipeline::PbrTextureRole::Unknown,
                    confidence: content_pipeline::PbrMatchConfidence::Unresolved,
                    included: false,
                },
            ],
        };

        assert_eq!(pbr_draft_group_count(&draft), 1);
    }

    #[test]
    fn gltf_extraction_review_rejects_duplicate_and_external_paths() {
        let asset = MaterialAsset {
            schema_version: shared::MATERIAL_ASSET_SCHEMA_VERSION,
            label: "Desk".into(),
            shader: shared::ShaderRefDesc::default(),
            shader_ref: None,
            render_state: Default::default(),
            provenance: Some(shared::MaterialProvenance {
                source_path: "assets/desk.gltf".into(),
                source_fingerprint: "abc".into(),
                source_sub_asset_id: "material:0".into(),
                source_label: "Desk".into(),
            }),
            inputs: Default::default(),
        };
        let entry = GltfMaterialExtractionEntry {
            included: true,
            source_index: 0,
            source_name: "Desk".into(),
            path: format!(
                "assets/extraction-review-{}/desk.material.ron",
                uuid::Uuid::new_v4()
            ),
            asset: asset.clone(),
            write_mode: MaterialExtractionWriteMode::CreateNew,
        };
        let mut draft = GltfMaterialExtractionDraft {
            source: "assets/desk.gltf".into(),
            destination: "assets".into(),
            materials: vec![entry.clone()],
            actor_target: None,
        };
        assert_eq!(gltf_extraction_validation_error(&draft), None);

        draft.materials.push(GltfMaterialExtractionEntry {
            source_index: 1,
            ..entry
        });
        assert!(gltf_extraction_validation_error(&draft)
            .is_some_and(|error| error.contains("same project path")));

        draft.materials.truncate(1);
        draft.materials[0].path = "../outside.material.ron".into();
        assert!(gltf_extraction_validation_error(&draft)
            .is_some_and(|error| error.contains("Invalid project path")));
    }

    #[test]
    fn gltf_extraction_existing_material_requires_reviewed_guarded_apply() {
        let folder = PathBuf::from(format!(
            "assets/.extraction-review-{}",
            uuid::Uuid::new_v4()
        ));
        fs::create_dir_all(&folder).unwrap();
        let path = folder.join("desk.material.ron");
        let provenance = shared::MaterialProvenance {
            source_path: "assets/desk.gltf".into(),
            source_fingerprint: "old".into(),
            source_sub_asset_id: "material:0".into(),
            source_label: "Desk".into(),
        };
        let existing = MaterialAsset {
            schema_version: shared::MATERIAL_ASSET_SCHEMA_VERSION,
            label: "Locally Edited Desk".into(),
            shader: shared::ShaderRefDesc::default(),
            shader_ref: None,
            render_state: Default::default(),
            provenance: Some(provenance.clone()),
            inputs: Default::default(),
        };
        fs::write(&path, ron::ser::to_string(&existing).unwrap()).unwrap();
        let mut incoming = existing.clone();
        incoming.label = "Desk".into();
        incoming.provenance.as_mut().unwrap().source_fingerprint = "new".into();
        let entry = GltfMaterialExtractionEntry {
            included: true,
            source_index: 0,
            source_name: "Desk".into(),
            path: path.to_string_lossy().replace('\\', "/"),
            asset: incoming,
            write_mode: MaterialExtractionWriteMode::Undecided,
        };
        let mut draft = GltfMaterialExtractionDraft {
            source: provenance.source_path,
            destination: folder.to_string_lossy().replace('\\', "/"),
            materials: vec![entry],
            actor_target: None,
        };

        assert!(gltf_extraction_validation_error(&draft)
            .is_some_and(|error| error.contains("choose Apply or Create New")));
        let review = review_existing_extraction_target(&draft.materials[0])
            .unwrap()
            .unwrap();
        assert!(review.provenance_matches);
        assert!(review.changes.contains(&"label"));
        assert!(review.changes.contains(&"source revision"));
        draft.materials[0].write_mode = MaterialExtractionWriteMode::ApplyExisting {
            expected_fingerprint: review.fingerprint,
            previous_bytes: review.bytes,
        };
        assert_eq!(gltf_extraction_validation_error(&draft), None);

        fs::write(&path, "external edit").unwrap();
        assert!(gltf_extraction_validation_error(&draft).is_some());
        fs::remove_dir_all(folder).unwrap();
    }

    #[test]
    fn actor_extraction_assignment_changes_only_the_exact_primitive_slot() {
        let mut app = App::new();
        app.add_plugins(shared::SharedTypesPlugin);
        app.init_resource::<crate::history::EditorHistory>();
        let entity = app
            .world_mut()
            .spawn((
                shared::LevelObject,
                shared::ActorKind::StaticMesh,
                shared::Primitive::default(),
            ))
            .id();
        let target = GltfMaterialExtractionTarget {
            entity,
            slot_id: shared::ComponentInstanceId::new(shared::PRIMITIVE_SURFACE_SLOT_ID),
            source_sub_asset_id: "material:0".into(),
        };
        let reference = MaterialRef::new(
            EditorAssetRef::new("material-id", "material:source", "Desk")
                .with_source_path("assets/Props/desk.material.ron"),
        );

        validate_actor_material_extraction_target(app.world(), &target).unwrap();
        assign_extracted_material_to_actor_slot(app.world_mut(), &target, reference.clone())
            .unwrap();

        assert_eq!(
            app.world()
                .get::<shared::Primitive>(entity)
                .unwrap()
                .surface
                .material
                .as_ref(),
            Some(&reference)
        );
        assert!(app
            .world()
            .resource::<crate::history::EditorHistory>()
            .can_undo());
    }

    #[test]
    fn extracted_material_binding_maps_every_draw_slot_using_the_source_material() {
        use crate::assets::static_mesh::{
            StaticMeshImportSnapshot, StaticMeshManifest, StaticMeshMetadata, StaticMeshPart,
            StaticMeshSource,
        };

        let model_path = "assets/Props/desk.gltf";
        let material_path = "assets/Props/desk.material.ron";
        let material_id = shared::AssetId::new();
        let mut document = AssetRegistryDocument {
            records: vec![
                shared::AssetRecord {
                    id: shared::AssetId::new(),
                    path: model_path.into(),
                    label: "Desk".into(),
                    kind: AssetKind::Model,
                    source_fingerprint: None,
                    import_settings: shared::AssetImportSettings::Model(ImportSettings::default()),
                    dependencies: Vec::new(),
                },
                shared::AssetRecord {
                    id: material_id.clone(),
                    path: material_path.into(),
                    label: "Desk Steel".into(),
                    kind: AssetKind::Material,
                    source_fingerprint: None,
                    import_settings: shared::AssetImportSettings::None,
                    dependencies: Vec::new(),
                },
            ],
            ..Default::default()
        };
        let part = |id: &str, material_index: usize| StaticMeshPart {
            id: id.into(),
            name: id.into(),
            mesh_label: format!("Mesh{material_index}/Primitive0"),
            material_id: Some(material_id_from_label(&format!("Material{material_index}"))),
            material_slot_name: "Desk Steel".into(),
            material_label: Some(format!("Material{material_index}")),
            local_transform: Transform::default(),
            source_node: None,
            source_mesh: None,
            source_material: Some("Desk Steel".into()),
            skinned: false,
        };
        let manifest = StaticMeshManifest {
            schema_version: crate::assets::static_mesh::STATIC_MESH_MANIFEST_SCHEMA,
            asset_id: document.records[0].id.as_string(),
            label: "Desk".into(),
            source: StaticMeshSource {
                path: model_path.into(),
                format: "gltf".into(),
                fingerprint: shared::AssetSourceFingerprint::from_bytes(b"desk"),
                dependencies: Vec::new(),
            },
            import: StaticMeshImportSnapshot {
                scale: 1.0,
                generate_collider: true,
                lod0_only: true,
                placement_mode: ModelPlacementMode::StaticAsset,
                hierarchy_mode: ModelHierarchyMode::SingleActor,
                material_policy: Default::default(),
                material_slots: Vec::new(),
                orphaned_material_slots: Vec::new(),
            },
            metadata: StaticMeshMetadata {
                mesh_count: 3,
                material_count: 2,
                node_count: 3,
                animation_count: 0,
                skin_count: 0,
                light_count: 0,
                camera_count: 0,
            },
            parts: vec![part("draw:a", 0), part("draw:b", 0), part("draw:c", 1)],
            warnings: Vec::new(),
        };

        let mapped = bind_extracted_material_slots(
            &mut document,
            model_path,
            &manifest,
            &[ExtractedMaterialBinding {
                source_index: 0,
                path: material_path.into(),
            }],
        )
        .unwrap();

        assert_eq!(mapped, 2);
        let slots = &document.records[0].model_import().material_slots;
        assert_eq!(slots.len(), 2);
        assert!(slots.iter().all(|slot| {
            matches!(
                &slot.selection,
                ModelMaterialSelection::Project(reference)
                    if reference.0.asset_id == material_id.as_string()
                        && reference.0.source_path.as_deref() == Some(material_path)
            )
        }));
    }
