//! DreamProcedures — procedural generation tools for the Dreamwell render pipeline.
//!
//! Extends the existing ProceduralGenerator (noise textures) with:
//! - Mesh generation from SDFs (marching cubes)
//! - Terrain heightmap -> mesh conversion
//! - Procedural material assignment (biome-based)
//! - Scene population helpers (scatter, grid, spiral patterns)
//! - FBX -> Dreamlet decomposition pipeline
//!
//! Clean Compute: all generation runs at import/load time (cold path).
//! Results are pre-allocated GPU buffers consumed by DVR or TRP render passes.

use crate::dreamlet_catalog::{GpuDreamlet, DREAMLET_FLAG_ACTIVE, DREAMLET_STATE_STATIC};
use crate::primitives::PbrVertex;

// ── Configuration ──────────────────────────────────────────────────────

/// Configuration for procedural mesh generation.
#[derive(Debug, Clone)]
pub struct ProceduralMeshConfig {
    /// Grid cells per axis (default: 64).
    pub resolution: u32,
    /// Extent in world units (default: 100.0).
    pub world_size: f32,
    /// Vertical scale multiplier (default: 10.0).
    pub height_scale: f32,
    /// Noise seed for deterministic generation.
    pub seed: u32,
    /// Noise octaves (default: 6).
    pub octaves: u32,
    /// Noise falloff per octave (default: 0.5).
    pub persistence: f32,
    /// Frequency multiplier per octave (default: 2.0).
    pub lacunarity: f32,
}

impl Default for ProceduralMeshConfig {
    fn default() -> Self {
        Self {
            resolution: 64,
            world_size: 100.0,
            height_scale: 10.0,
            seed: 0,
            octaves: 6,
            persistence: 0.5,
            lacunarity: 2.0,
        }
    }
}

// ── Noise helpers (CPU-side, cold-path) ────────────────────────────────

/// PCG hash for deterministic noise seeding.
fn pcg_hash(input: u32) -> u32 {
    let state = input.wrapping_mul(747796405).wrapping_add(2891336453);
    let word = ((state >> ((state >> 28).wrapping_add(4))) ^ state).wrapping_mul(277803737);
    (word >> 22) ^ word
}

/// Simplex-style gradient noise (2D). Returns value in approximately [-1, 1].
fn gradient_noise_2d(x: f32, y: f32, seed: u32) -> f32 {
    let ix = x.floor() as i32;
    let iy = y.floor() as i32;
    let fx = x - x.floor();
    let fy = y - y.floor();

    // Smoothstep interpolation
    let ux = fx * fx * (3.0 - 2.0 * fx);
    let uy = fy * fy * (3.0 - 2.0 * fy);

    // Hash corners
    let hash = |cx: i32, cy: i32| -> f32 {
        let h = pcg_hash(pcg_hash((cx as u32).wrapping_add(seed)) ^ (cy as u32));
        // Convert to [-1, 1]
        (h as f32 / u32::MAX as f32) * 2.0 - 1.0
    };

    let n00 = hash(ix, iy);
    let n10 = hash(ix + 1, iy);
    let n01 = hash(ix, iy + 1);
    let n11 = hash(ix + 1, iy + 1);

    // Bilinear interpolation
    let nx0 = n00 + (n10 - n00) * ux;
    let nx1 = n01 + (n11 - n01) * ux;
    nx0 + (nx1 - nx0) * uy
}

/// FBM (Fractal Brownian Motion) noise — CPU-side for procedural generation.
/// Returns value in approximately [-1, 1].
pub fn fbm_noise_2d(x: f32, y: f32, octaves: u32, persistence: f32, lacunarity: f32, seed: u32) -> f32 {
    let mut value = 0.0_f32;
    let mut amplitude = 1.0_f32;
    let mut frequency = 1.0_f32;
    let mut max_amplitude = 0.0_f32;

    for i in 0..octaves {
        let octave_seed = pcg_hash(seed.wrapping_add(i));
        value += gradient_noise_2d(x * frequency, y * frequency, octave_seed) * amplitude;
        max_amplitude += amplitude;
        amplitude *= persistence;
        frequency *= lacunarity;
    }

    if max_amplitude > 0.0 {
        value / max_amplitude
    } else {
        0.0
    }
}

// ── HeightmapTerrain ───────────────────────────────────────────────────

/// A terrain mesh generated from a heightmap or noise.
/// Vertices use PbrVertex (64 bytes) for direct upload to the PBR pipeline.
pub struct HeightmapTerrain {
    pub vertices: Vec<PbrVertex>,
    pub indices: Vec<u32>,
    pub width: u32,
    pub height: u32,
}

impl HeightmapTerrain {
    /// Generate terrain from config using CPU-side FBM noise.
    pub fn generate(config: &ProceduralMeshConfig) -> Self {
        let res = config.resolution;
        let cell_size = config.world_size / res as f32;
        let half_size = config.world_size * 0.5;
        let vert_count = ((res + 1) * (res + 1)) as usize;
        let index_count = (res * res * 6) as usize;

        let mut vertices = Vec::with_capacity(vert_count);
        let mut heights = Vec::with_capacity(vert_count);

        // Generate height values and vertex positions
        for z in 0..=res {
            for x in 0..=res {
                let wx = x as f32 * cell_size - half_size;
                let wz = z as f32 * cell_size - half_size;

                // Normalize coords to [0, ~10] range for noise sampling
                let nx = x as f32 / res as f32 * 10.0;
                let nz = z as f32 / res as f32 * 10.0;

                let h = fbm_noise_2d(
                    nx,
                    nz,
                    config.octaves,
                    config.persistence,
                    config.lacunarity,
                    config.seed,
                ) * config.height_scale;

                heights.push(h);

                let u = x as f32 / res as f32;
                let v = z as f32 / res as f32;

                vertices.push(PbrVertex {
                    position: [wx, h, wz],
                    normal: [0.0, 1.0, 0.0], // placeholder, computed below
                    tangent: [1.0, 0.0, 0.0, 1.0],
                    tex_coord: [u, v],
                    color: [1.0, 1.0, 1.0, 1.0],
                });
            }
        }

        // Generate indices
        let mut indices = Vec::with_capacity(index_count);
        let stride = res + 1;
        for z in 0..res {
            for x in 0..res {
                let tl = z * stride + x;
                let tr = tl + 1;
                let bl = (z + 1) * stride + x;
                let br = bl + 1;
                indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
            }
        }

        // Compute normals from adjacent height samples
        Self::compute_normals(&mut vertices, &heights, res + 1, res + 1, cell_size);

        // Compute tangents from UVs
        Self::compute_tangents_from_indices(&mut vertices, &indices);

        Self {
            vertices,
            indices,
            width: res + 1,
            height: res + 1,
        }
    }

    /// Generate terrain from a heightmap image (R channel = height).
    /// `data` is a row-major array of bytes, one per pixel.
    pub fn from_heightmap(data: &[u8], width: u32, height: u32, height_scale: f32) -> Self {
        let expected = (width * height) as usize;
        let data_len = data.len().min(expected);

        let cell_size = 1.0;
        let half_w = width as f32 * 0.5;
        let half_h = height as f32 * 0.5;

        let vert_count = (width * height) as usize;
        let mut vertices = Vec::with_capacity(vert_count);
        let mut heights = Vec::with_capacity(vert_count);

        for z in 0..height {
            for x in 0..width {
                let idx = (z * width + x) as usize;
                let h = if idx < data_len {
                    (data[idx] as f32 / 255.0) * height_scale
                } else {
                    0.0
                };

                heights.push(h);

                let wx = x as f32 - half_w;
                let wz = z as f32 - half_h;
                let u = x as f32 / (width.max(1) - 1).max(1) as f32;
                let v = z as f32 / (height.max(1) - 1).max(1) as f32;

                vertices.push(PbrVertex {
                    position: [wx, h, wz],
                    normal: [0.0, 1.0, 0.0],
                    tangent: [1.0, 0.0, 0.0, 1.0],
                    tex_coord: [u, v],
                    color: [1.0, 1.0, 1.0, 1.0],
                });
            }
        }

        // Generate indices (skip if width or height < 2)
        let mut indices = Vec::new();
        if width >= 2 && height >= 2 {
            indices.reserve(((width - 1) * (height - 1) * 6) as usize);
            for z in 0..height - 1 {
                for x in 0..width - 1 {
                    let tl = z * width + x;
                    let tr = tl + 1;
                    let bl = (z + 1) * width + x;
                    let br = bl + 1;
                    indices.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
                }
            }
        }

        Self::compute_normals(&mut vertices, &heights, width, height, cell_size);
        Self::compute_tangents_from_indices(&mut vertices, &indices);

        Self {
            vertices,
            indices,
            width,
            height,
        }
    }

    /// Compute vertex normals from height field using finite differences.
    fn compute_normals(vertices: &mut [PbrVertex], heights: &[f32], width: u32, height: u32, cell_size: f32) {
        for z in 0..height {
            for x in 0..width {
                let idx = (z * width + x) as usize;
                let h_l = if x > 0 { heights[idx - 1] } else { heights[idx] };
                let h_r = if x < width - 1 { heights[idx + 1] } else { heights[idx] };
                let h_d = if z > 0 {
                    heights[idx - width as usize]
                } else {
                    heights[idx]
                };
                let h_u = if z < height - 1 {
                    heights[idx + width as usize]
                } else {
                    heights[idx]
                };

                let nx = (h_l - h_r) / (2.0 * cell_size);
                let nz = (h_d - h_u) / (2.0 * cell_size);
                let ny = 1.0_f32;

                let len = (nx * nx + ny * ny + nz * nz).sqrt();
                if len > 1e-8 {
                    vertices[idx].normal = [nx / len, ny / len, nz / len];
                }
            }
        }
    }

    /// Compute tangent vectors from triangle edges and UVs.
    fn compute_tangents_from_indices(vertices: &mut [PbrVertex], indices: &[u32]) {
        let n = vertices.len();
        let mut tan_accum: Vec<[f32; 3]> = vec![[0.0; 3]; n];

        for tri in indices.chunks(3) {
            if tri.len() < 3 {
                continue;
            }
            let i0 = tri[0] as usize;
            let i1 = tri[1] as usize;
            let i2 = tri[2] as usize;
            if i0 >= n || i1 >= n || i2 >= n {
                continue;
            }

            let p0 = vertices[i0].position;
            let p1 = vertices[i1].position;
            let p2 = vertices[i2].position;

            let uv0 = vertices[i0].tex_coord;
            let uv1 = vertices[i1].tex_coord;
            let uv2 = vertices[i2].tex_coord;

            let dp1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
            let dp2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
            let duv1 = [uv1[0] - uv0[0], uv1[1] - uv0[1]];
            let duv2 = [uv2[0] - uv0[0], uv2[1] - uv0[1]];

            let det = duv1[0] * duv2[1] - duv1[1] * duv2[0];
            let r = if det.abs() > 1e-8 { 1.0 / det } else { 0.0 };

            let t = [
                (duv2[1] * dp1[0] - duv1[1] * dp2[0]) * r,
                (duv2[1] * dp1[1] - duv1[1] * dp2[1]) * r,
                (duv2[1] * dp1[2] - duv1[1] * dp2[2]) * r,
            ];

            for &idx in &[i0, i1, i2] {
                tan_accum[idx][0] += t[0];
                tan_accum[idx][1] += t[1];
                tan_accum[idx][2] += t[2];
            }
        }

        for (i, v) in vertices.iter_mut().enumerate() {
            let t = tan_accum[i];
            let n = v.normal;

            // Gram-Schmidt orthogonalize
            let dot_nt = n[0] * t[0] + n[1] * t[1] + n[2] * t[2];
            let orth = [t[0] - n[0] * dot_nt, t[1] - n[1] * dot_nt, t[2] - n[2] * dot_nt];
            let len = (orth[0] * orth[0] + orth[1] * orth[1] + orth[2] * orth[2]).sqrt();

            if len > 1e-8 {
                v.tangent = [orth[0] / len, orth[1] / len, orth[2] / len, 1.0];
            } else {
                // Fallback tangent perpendicular to normal
                if n[1].abs() > 0.9 {
                    v.tangent = [1.0, 0.0, 0.0, 1.0];
                } else {
                    v.tangent = [0.0, 1.0, 0.0, 1.0];
                }
            }
        }
    }
}

// ── ScatterPattern ─────────────────────────────────────────────────────

/// Placement pattern for scattering objects in a scene.
#[derive(Debug, Clone)]
pub enum ScatterPattern {
    /// Regular grid placement.
    Grid { spacing: f32 },
    /// Pseudo-random placement within a radius.
    Random { count: u32, seed: u32, radius: f32 },
    /// Archimedean spiral.
    Spiral { count: u32, spacing: f32, height_step: f32 },
    /// Evenly spaced around a circle.
    Circle { count: u32, radius: f32 },
}

impl ScatterPattern {
    /// Generate world-space positions for object placement.
    pub fn generate_positions(&self, center: [f32; 3]) -> Vec<[f32; 3]> {
        match self {
            ScatterPattern::Grid { spacing } => {
                // Generate a grid centered on the center point.
                // Grid extent: 10x10 cells (fixed extent, spacing controls density).
                let half_extent = 5;
                let mut positions = Vec::new();
                for z in -half_extent..=half_extent {
                    for x in -half_extent..=half_extent {
                        positions.push([
                            center[0] + x as f32 * spacing,
                            center[1],
                            center[2] + z as f32 * spacing,
                        ]);
                    }
                }
                positions
            }
            ScatterPattern::Random { count, seed, radius } => {
                let mut positions = Vec::with_capacity(*count as usize);
                let mut hash_state = *seed;
                for _ in 0..*count {
                    // Generate two uniform random values in [0, 1)
                    hash_state = pcg_hash(hash_state);
                    let u = hash_state as f32 / u32::MAX as f32;
                    hash_state = pcg_hash(hash_state);
                    let v = hash_state as f32 / u32::MAX as f32;

                    // Uniform distribution on a disc via square-root mapping
                    let angle = u * std::f32::consts::TAU;
                    let r = v.sqrt() * radius;
                    positions.push([center[0] + r * angle.cos(), center[1], center[2] + r * angle.sin()]);
                }
                positions
            }
            ScatterPattern::Spiral {
                count,
                spacing,
                height_step,
            } => {
                let mut positions = Vec::with_capacity(*count as usize);
                for i in 0..*count {
                    let t = i as f32;
                    let angle = t * 0.5; // half-radian increment per step
                    let r = t * spacing;
                    positions.push([
                        center[0] + r * angle.cos(),
                        center[1] + t * height_step,
                        center[2] + r * angle.sin(),
                    ]);
                }
                positions
            }
            ScatterPattern::Circle { count, radius } => {
                if *count == 0 {
                    return Vec::new();
                }
                let mut positions = Vec::with_capacity(*count as usize);
                for i in 0..*count {
                    let angle = (i as f32 / *count as f32) * std::f32::consts::TAU;
                    positions.push([
                        center[0] + radius * angle.cos(),
                        center[1],
                        center[2] + radius * angle.sin(),
                    ]);
                }
                positions
            }
        }
    }
}

// ── FbxDecomposer ──────────────────────────────────────────────────────

/// Decomposes FBX import results into GPU dreamlet descriptors.
/// Each triangle cluster becomes a dreamlet with position at centroid,
/// scale from bounding radius, and material from the caller.
pub struct FbxDecomposer;

impl FbxDecomposer {
    /// Convert an FBX import result into an array of GpuDreamlet descriptors.
    /// Each triangle cluster becomes a dreamlet with position at centroid,
    /// scale from bounding radius, and material from the source mesh.
    ///
    /// `cluster_size` is the number of triangles per dreamlet (default: 64).
    pub fn decompose(
        import: &dreamwell_engine::fbx::FbxImportResult,
        cluster_size: u32,
        base_color: [f32; 4],
        roughness: f32,
        metallic: f32,
    ) -> Vec<GpuDreamlet> {
        if import.indices.is_empty() || import.vertices.is_empty() {
            return Vec::new();
        }

        let tri_count = import.indices.len() / 3;
        let cluster_tris = (cluster_size.max(1)) as usize;
        let num_clusters = (tri_count + cluster_tris - 1) / cluster_tris;

        let mut dreamlets = Vec::with_capacity(num_clusters);

        for cluster_idx in 0..num_clusters {
            let start_tri = cluster_idx * cluster_tris;
            let end_tri = (start_tri + cluster_tris).min(tri_count);

            // Compute centroid and bounding radius
            let mut cx = 0.0_f32;
            let mut cy = 0.0_f32;
            let mut cz = 0.0_f32;
            let mut vert_count = 0u32;

            for tri in start_tri..end_tri {
                for k in 0..3 {
                    let vi = import.indices[tri * 3 + k] as usize;
                    if vi < import.vertices.len() {
                        let pos = import.vertices[vi].position;
                        cx += pos[0];
                        cy += pos[1];
                        cz += pos[2];
                        vert_count += 1;
                    }
                }
            }

            if vert_count == 0 {
                continue;
            }

            let inv_count = 1.0 / vert_count as f32;
            cx *= inv_count;
            cy *= inv_count;
            cz *= inv_count;

            // Compute bounding radius from centroid
            let mut max_dist_sq = 0.0_f32;
            for tri in start_tri..end_tri {
                for k in 0..3 {
                    let vi = import.indices[tri * 3 + k] as usize;
                    if vi < import.vertices.len() {
                        let pos = import.vertices[vi].position;
                        let dx = pos[0] - cx;
                        let dy = pos[1] - cy;
                        let dz = pos[2] - cz;
                        let dist_sq = dx * dx + dy * dy + dz * dz;
                        if dist_sq > max_dist_sq {
                            max_dist_sq = dist_sq;
                        }
                    }
                }
            }

            let bounding_radius = max_dist_sq.sqrt();
            let scale = bounding_radius.max(0.001);

            dreamlets.push(GpuDreamlet {
                position: [cx, cy, cz],
                scale,
                velocity: [0.0; 3],
                angular_velocity: 0.0,
                rotation: [0.0, 0.0, 0.0, 1.0], // identity quaternion
                base_color,
                roughness,
                metallic,
                mass: 1.0,
                restitution: 0.3,
                state: DREAMLET_STATE_STATIC,
                mesh_id: cluster_idx as u32,
                parent_id: 0,
                flags: DREAMLET_FLAG_ACTIVE,
                age: 0.0,
                lifetime: f32::MAX,
                _pad_target: [0.0; 2],
                target_position: [cx, cy, cz],
                deconstruct_level: 0,
                bounding_radius,
                skeleton_index: 0,
                _pad_struct: [0.0; 2],
            });
        }

        dreamlets
    }
}

// ── BiomeMaterial ──────────────────────────────────────────────────────

/// Terrain biome classification for procedural material assignment.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Biome {
    Grass,
    Rock,
    Sand,
    Snow,
    Water,
    Forest,
    Desert,
    Tundra,
}

impl Biome {
    /// Get a PbrMaterial preset for this biome.
    pub fn material(&self) -> dreamwell_engine::material::PbrMaterial {
        use dreamwell_engine::material::PbrMaterial;
        match self {
            Biome::Grass => PbrMaterial {
                base_color: [0.2, 0.55, 0.15, 1.0],
                roughness: 0.85,
                metallic: 0.0,
                ..PbrMaterial::default()
            },
            Biome::Rock => PbrMaterial {
                base_color: [0.45, 0.42, 0.38, 1.0],
                roughness: 0.9,
                metallic: 0.0,
                ..PbrMaterial::default()
            },
            Biome::Sand => PbrMaterial {
                base_color: [0.85, 0.78, 0.55, 1.0],
                roughness: 0.95,
                metallic: 0.0,
                ..PbrMaterial::default()
            },
            Biome::Snow => PbrMaterial {
                base_color: [0.95, 0.95, 0.97, 1.0],
                roughness: 0.7,
                metallic: 0.0,
                ..PbrMaterial::default()
            },
            Biome::Water => PbrMaterial {
                base_color: [0.1, 0.3, 0.6, 0.8],
                roughness: 0.05,
                metallic: 0.0,
                alpha_mode: dreamwell_engine::material::AlphaMode::Blend,
                ..PbrMaterial::default()
            },
            Biome::Forest => PbrMaterial {
                base_color: [0.15, 0.4, 0.1, 1.0],
                roughness: 0.8,
                metallic: 0.0,
                ..PbrMaterial::default()
            },
            Biome::Desert => PbrMaterial {
                base_color: [0.9, 0.75, 0.4, 1.0],
                roughness: 0.95,
                metallic: 0.0,
                ..PbrMaterial::default()
            },
            Biome::Tundra => PbrMaterial {
                base_color: [0.6, 0.6, 0.55, 1.0],
                roughness: 0.85,
                metallic: 0.0,
                ..PbrMaterial::default()
            },
        }
    }

    /// Select biome based on height (normalized 0..1) and slope (radians).
    pub fn from_terrain(height: f32, slope: f32) -> Self {
        // Slope threshold for rock (approximately 45 degrees)
        const ROCK_SLOPE: f32 = 0.785;
        // Height thresholds (normalized)
        const WATER_LINE: f32 = 0.1;
        const SAND_LINE: f32 = 0.2;
        const FOREST_LINE: f32 = 0.5;
        const SNOW_LINE: f32 = 0.85;
        const TUNDRA_LINE: f32 = 0.75;

        if slope > ROCK_SLOPE {
            return Biome::Rock;
        }

        if height < WATER_LINE {
            Biome::Water
        } else if height < SAND_LINE {
            Biome::Sand
        } else if height < FOREST_LINE {
            Biome::Grass
        } else if height < TUNDRA_LINE {
            Biome::Forest
        } else if height < SNOW_LINE {
            Biome::Tundra
        } else {
            Biome::Snow
        }
    }
}

// ── Tests ──────────────────────────────────────────────────────────────

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

    #[test]
    fn heightmap_terrain_generate_produces_valid_mesh() {
        let config = ProceduralMeshConfig {
            resolution: 8,
            world_size: 10.0,
            ..Default::default()
        };
        let terrain = HeightmapTerrain::generate(&config);

        // (res+1)^2 vertices
        let expected_verts = (8 + 1) * (8 + 1);
        assert_eq!(terrain.vertices.len(), expected_verts);
        assert_eq!(terrain.width, 9);
        assert_eq!(terrain.height, 9);

        // res^2 * 6 indices (2 triangles per quad, 3 indices per triangle)
        let expected_indices = 8 * 8 * 6;
        assert_eq!(terrain.indices.len(), expected_indices);

        // All indices divisible by 3 (triangle list)
        assert_eq!(terrain.indices.len() % 3, 0);

        // No degenerate indices
        let max_idx = terrain.vertices.len() as u32;
        for &idx in &terrain.indices {
            assert!(idx < max_idx, "index {} out of bounds (max {})", idx, max_idx);
        }
    }

    #[test]
    fn heightmap_terrain_normals_are_normalized() {
        let config = ProceduralMeshConfig {
            resolution: 4,
            ..Default::default()
        };
        let terrain = HeightmapTerrain::generate(&config);

        for (i, v) in terrain.vertices.iter().enumerate() {
            let n = v.normal;
            let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
            assert!(
                (len - 1.0).abs() < 0.01,
                "vertex {} normal not normalized (len={})",
                i,
                len
            );
        }
    }

    #[test]
    fn heightmap_from_image_produces_valid_mesh() {
        let data = vec![128u8; 16]; // 4x4 heightmap
        let terrain = HeightmapTerrain::from_heightmap(&data, 4, 4, 5.0);

        assert_eq!(terrain.vertices.len(), 16);
        assert_eq!(terrain.width, 4);
        assert_eq!(terrain.height, 4);
        assert_eq!(terrain.indices.len(), 3 * 3 * 6); // (4-1)*(4-1)*6

        // All heights should be the same (uniform input)
        let expected_h = (128.0 / 255.0) * 5.0;
        for v in &terrain.vertices {
            assert!(
                (v.position[1] - expected_h).abs() < 0.01,
                "height mismatch: {} vs {}",
                v.position[1],
                expected_h
            );
        }
    }

    #[test]
    fn scatter_grid_produces_expected_count() {
        let pattern = ScatterPattern::Grid { spacing: 2.0 };
        let positions = pattern.generate_positions([0.0, 0.0, 0.0]);
        // 11x11 grid (from -5 to +5 inclusive)
        assert_eq!(positions.len(), 121);
    }

    #[test]
    fn scatter_random_is_deterministic_with_same_seed() {
        let pattern = ScatterPattern::Random {
            count: 50,
            seed: 42,
            radius: 10.0,
        };
        let a = pattern.generate_positions([0.0, 0.0, 0.0]);
        let b = pattern.generate_positions([0.0, 0.0, 0.0]);

        assert_eq!(a.len(), 50);
        assert_eq!(a.len(), b.len());
        for (pa, pb) in a.iter().zip(b.iter()) {
            assert_eq!(pa, pb, "random scatter not deterministic");
        }
    }

    #[test]
    fn scatter_random_within_radius() {
        let radius = 5.0;
        let pattern = ScatterPattern::Random {
            count: 100,
            seed: 7,
            radius,
        };
        let center = [10.0, 0.0, 20.0];
        let positions = pattern.generate_positions(center);

        for (i, pos) in positions.iter().enumerate() {
            let dx = pos[0] - center[0];
            let dz = pos[2] - center[2];
            let dist = (dx * dx + dz * dz).sqrt();
            assert!(
                dist <= radius + 0.001,
                "position {} at distance {} exceeds radius {}",
                i,
                dist,
                radius
            );
        }
    }

    #[test]
    fn scatter_spiral_count() {
        let pattern = ScatterPattern::Spiral {
            count: 20,
            spacing: 0.5,
            height_step: 0.1,
        };
        let positions = pattern.generate_positions([0.0, 0.0, 0.0]);
        assert_eq!(positions.len(), 20);
    }

    #[test]
    fn scatter_circle_count() {
        let pattern = ScatterPattern::Circle { count: 8, radius: 5.0 };
        let positions = pattern.generate_positions([0.0, 0.0, 0.0]);
        assert_eq!(positions.len(), 8);

        // All positions should be at the specified radius
        for (i, pos) in positions.iter().enumerate() {
            let dist = (pos[0] * pos[0] + pos[2] * pos[2]).sqrt();
            assert!(
                (dist - 5.0).abs() < 0.01,
                "circle position {} at distance {} (expected 5.0)",
                i,
                dist
            );
        }
    }

    #[test]
    fn scatter_circle_zero_count() {
        let pattern = ScatterPattern::Circle { count: 0, radius: 5.0 };
        let positions = pattern.generate_positions([0.0, 0.0, 0.0]);
        assert!(positions.is_empty());
    }

    #[test]
    fn biome_from_terrain_height_slope_consistent() {
        // Water at low height
        assert_eq!(Biome::from_terrain(0.05, 0.0), Biome::Water);
        // Sand near shoreline
        assert_eq!(Biome::from_terrain(0.15, 0.0), Biome::Sand);
        // Grass at mid height
        assert_eq!(Biome::from_terrain(0.35, 0.0), Biome::Grass);
        // Forest above grass
        assert_eq!(Biome::from_terrain(0.6, 0.0), Biome::Forest);
        // Tundra at higher elevation
        assert_eq!(Biome::from_terrain(0.8, 0.0), Biome::Tundra);
        // Snow at top
        assert_eq!(Biome::from_terrain(0.9, 0.0), Biome::Snow);
        // Steep slope always gives rock
        assert_eq!(Biome::from_terrain(0.5, 1.0), Biome::Rock);
    }

    #[test]
    fn biome_materials_have_valid_roughness() {
        let biomes = [
            Biome::Grass,
            Biome::Rock,
            Biome::Sand,
            Biome::Snow,
            Biome::Water,
            Biome::Forest,
            Biome::Desert,
            Biome::Tundra,
        ];
        for biome in &biomes {
            let mat = biome.material();
            assert!(
                (0.0..=1.0).contains(&mat.roughness),
                "{:?} roughness out of range: {}",
                biome,
                mat.roughness
            );
            assert!(
                (0.0..=1.0).contains(&mat.metallic),
                "{:?} metallic out of range: {}",
                biome,
                mat.metallic
            );
        }
    }

    #[test]
    fn fbm_noise_range() {
        // FBM output should be in approximately [-1, 1]
        let mut min_val = f32::MAX;
        let mut max_val = f32::MIN;

        for i in 0..1000 {
            let x = (i as f32 / 100.0) - 5.0;
            for j in 0..100 {
                let y = (j as f32 / 10.0) - 5.0;
                let val = fbm_noise_2d(x, y, 6, 0.5, 2.0, 42);
                assert!(val.is_finite(), "FBM produced non-finite at ({}, {})", x, y);
                if val < min_val {
                    min_val = val;
                }
                if val > max_val {
                    max_val = val;
                }
            }
        }

        assert!(min_val >= -1.0 - 0.01, "FBM min {} below -1.0", min_val);
        assert!(max_val <= 1.0 + 0.01, "FBM max {} above 1.0", max_val);
    }

    #[test]
    fn fbm_noise_deterministic() {
        let a = fbm_noise_2d(3.14, 2.72, 4, 0.5, 2.0, 99);
        let b = fbm_noise_2d(3.14, 2.72, 4, 0.5, 2.0, 99);
        assert_eq!(a, b, "FBM noise not deterministic");
    }

    #[test]
    fn fbm_noise_different_seeds_differ() {
        let a = fbm_noise_2d(1.0, 1.0, 4, 0.5, 2.0, 0);
        let b = fbm_noise_2d(1.0, 1.0, 4, 0.5, 2.0, 1);
        // Different seeds should almost always produce different values
        assert_ne!(a, b, "FBM with different seeds produced identical output");
    }

    #[test]
    fn fbx_decomposer_valid_triangle() {
        use dreamwell_engine::fbx::{fbx_vertex, FbxImportResult};

        let import = FbxImportResult {
            vertices: vec![
                fbx_vertex([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
                fbx_vertex([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
                fbx_vertex([0.0, 1.0, 0.0], [0.0, 1.0, 0.0]),
            ],
            indices: vec![0, 1, 2],
            ..Default::default()
        };

        let dreamlets = FbxDecomposer::decompose(&import, 64, [1.0, 0.0, 0.0, 1.0], 0.5, 0.0);

        assert_eq!(dreamlets.len(), 1);

        let d = &dreamlets[0];
        // Centroid should be at (1/3, 1/3, 0)
        assert!((d.position[0] - 1.0 / 3.0).abs() < 0.01);
        assert!((d.position[1] - 1.0 / 3.0).abs() < 0.01);
        assert!((d.position[2] - 0.0).abs() < 0.01);

        // Identity rotation
        assert_eq!(d.rotation, [0.0, 0.0, 0.0, 1.0]);

        // Material properties propagated
        assert_eq!(d.base_color, [1.0, 0.0, 0.0, 1.0]);
        assert_eq!(d.roughness, 0.5);
        assert_eq!(d.metallic, 0.0);

        // Active and static
        assert_eq!(d.state, DREAMLET_STATE_STATIC);
        assert_eq!(d.flags, DREAMLET_FLAG_ACTIVE);

        // Bounding radius should be positive
        assert!(d.bounding_radius > 0.0);
        assert!(d.scale > 0.0);
    }

    #[test]
    fn fbx_decomposer_empty_import() {
        let import = dreamwell_engine::fbx::FbxImportResult::default();
        let dreamlets = FbxDecomposer::decompose(&import, 64, [1.0; 4], 0.5, 0.5);
        assert!(dreamlets.is_empty());
    }

    #[test]
    fn fbx_decomposer_multiple_clusters() {
        use dreamwell_engine::fbx::{fbx_vertex, FbxImportResult};

        // 4 triangles, cluster size 2 -> 2 clusters
        let import = FbxImportResult {
            vertices: vec![
                fbx_vertex([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
                fbx_vertex([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
                fbx_vertex([0.0, 1.0, 0.0], [0.0, 1.0, 0.0]),
                fbx_vertex([1.0, 1.0, 0.0], [0.0, 1.0, 0.0]),
                fbx_vertex([2.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
                fbx_vertex([2.0, 1.0, 0.0], [0.0, 1.0, 0.0]),
            ],
            indices: vec![
                0, 1, 2, // tri 0
                1, 3, 2, // tri 1
                1, 4, 3, // tri 2
                4, 5, 3, // tri 3
            ],
            ..Default::default()
        };

        let dreamlets = FbxDecomposer::decompose(&import, 2, [0.5; 4], 0.3, 0.7);
        assert_eq!(dreamlets.len(), 2);

        // Each cluster should have distinct positions
        assert_ne!(dreamlets[0].position, dreamlets[1].position);
    }

    #[test]
    fn procedural_mesh_config_default() {
        let config = ProceduralMeshConfig::default();
        assert_eq!(config.resolution, 64);
        assert!((config.world_size - 100.0).abs() < 0.01);
        assert!((config.height_scale - 10.0).abs() < 0.01);
        assert_eq!(config.octaves, 6);
        assert!((config.persistence - 0.5).abs() < 0.01);
        assert!((config.lacunarity - 2.0).abs() < 0.01);
    }

    #[test]
    fn pcg_hash_deterministic() {
        assert_eq!(pcg_hash(42), pcg_hash(42));
        assert_ne!(pcg_hash(0), pcg_hash(1));
    }
}
