//! Blacksite Surface ABI v1.
//!
//! The authoring layer persists named values. This crate validates and packs them into a fixed GPU
//! contract and supplies the same surface evaluator source to raster and Solari integrations.

use std::collections::HashMap;
use std::fs;

use bevy::mesh::MeshVertexBufferLayoutRef;
use bevy::pbr::{
    ExtendedMaterial, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline,
};
use bevy::prelude::*;
use bevy::render::extract_resource::ExtractResource;
use bevy::render::render_resource::{
    AsBindGroup, RenderPipelineDescriptor, ShaderType, SpecializedMeshPipelineError,
};
use bevy::shader::{Shader, ShaderRef};
use shared::{
    asset_server_path, material_from_desc, HydratedRendererMaterialBinding, MaterialAsset,
    MaterialInstanceAsset, MaterialParameterValue, MaterialRef, ShaderPropertyType,
    ShaderSchemaAsset, MATERIAL_INSTANCE_SCHEMA_VERSION,
};

pub const SURFACE_ABI_VERSION: u32 = 1;
pub const SURFACE_PARAMETER_LANES: usize = 16;
pub const SURFACE_TEXTURE_SLOTS: usize = 8;
pub const DEFAULT_SURFACE_SHADER_PATH: &str = "shaders/blacksite_surface.wgsl";

#[derive(ShaderType, Reflect, Debug, Clone, Copy, PartialEq)]
pub struct SurfaceUniform {
    pub shader_id: u32,
    pub flags: u32,
    pub alpha_cutoff: f32,
    pub abi_version: u32,
    pub params: [Vec4; SURFACE_PARAMETER_LANES],
    pub uv_transforms: [Vec4; SURFACE_TEXTURE_SLOTS],
}

impl Default for SurfaceUniform {
    fn default() -> Self {
        Self {
            shader_id: 0,
            flags: 0,
            alpha_cutoff: 0.5,
            abi_version: SURFACE_ABI_VERSION,
            params: [Vec4::ZERO; SURFACE_PARAMETER_LANES],
            uv_transforms: [Vec4::new(1.0, 1.0, 0.0, 0.0); SURFACE_TEXTURE_SLOTS],
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SurfaceExtensionKey {
    shader: AssetId<Shader>,
}

impl From<&SurfaceExtension> for SurfaceExtensionKey {
    fn from(extension: &SurfaceExtension) -> Self {
        Self {
            shader: extension.shader.id(),
        }
    }
}

#[derive(Asset, AsBindGroup, Reflect, Debug, Clone, Default)]
#[bind_group_data(SurfaceExtensionKey)]
pub struct SurfaceExtension {
    #[uniform(100)]
    pub uniform: SurfaceUniform,
    #[texture(101)]
    #[sampler(102)]
    pub texture0: Option<Handle<Image>>,
    #[texture(103)]
    #[sampler(104)]
    pub texture1: Option<Handle<Image>>,
    #[texture(105)]
    #[sampler(106)]
    pub texture2: Option<Handle<Image>>,
    #[texture(107)]
    #[sampler(108)]
    pub texture3: Option<Handle<Image>>,
    #[texture(109)]
    #[sampler(110)]
    pub texture4: Option<Handle<Image>>,
    #[texture(111)]
    #[sampler(112)]
    pub texture5: Option<Handle<Image>>,
    #[texture(113)]
    #[sampler(114)]
    pub texture6: Option<Handle<Image>>,
    #[texture(115)]
    #[sampler(116)]
    pub texture7: Option<Handle<Image>>,
    #[reflect(ignore)]
    pub shader: Handle<Shader>,
}

impl SurfaceExtension {
    pub fn set_texture(&mut self, index: usize, handle: Option<Handle<Image>>) {
        match index {
            0 => self.texture0 = handle,
            1 => self.texture1 = handle,
            2 => self.texture2 = handle,
            3 => self.texture3 = handle,
            4 => self.texture4 = handle,
            5 => self.texture5 = handle,
            6 => self.texture6 = handle,
            7 => self.texture7 = handle,
            _ => {}
        }
    }
}

impl MaterialExtension for SurfaceExtension {
    fn fragment_shader() -> ShaderRef {
        DEFAULT_SURFACE_SHADER_PATH.into()
    }

    fn deferred_fragment_shader() -> ShaderRef {
        DEFAULT_SURFACE_SHADER_PATH.into()
    }

    fn prepass_fragment_shader() -> ShaderRef {
        DEFAULT_SURFACE_SHADER_PATH.into()
    }

    fn specialize(
        _pipeline: &MaterialExtensionPipeline,
        descriptor: &mut RenderPipelineDescriptor,
        _layout: &MeshVertexBufferLayoutRef,
        key: MaterialExtensionKey<Self>,
    ) -> Result<(), SpecializedMeshPipelineError> {
        if let (AssetId::Uuid { uuid }, Some(fragment)) =
            (key.bind_group_data.shader, descriptor.fragment.as_mut())
        {
            fragment.shader = Handle::Uuid(uuid, Default::default());
        }
        Ok(())
    }
}

pub type SurfaceMaterial = ExtendedMaterial<StandardMaterial, SurfaceExtension>;

#[derive(Resource, Default)]
pub struct SurfaceMaterialCache {
    handles: HashMap<MaterialRef, Handle<SurfaceMaterial>>,
    standard_handles: HashMap<MaterialRef, Handle<StandardMaterial>>,
    revisions: HashMap<MaterialRef, u64>,
    standard_only: HashMap<MaterialRef, u64>,
    dependencies: HashMap<MaterialRef, Vec<String>>,
    failed_revisions: HashMap<MaterialRef, u64>,
}

enum BuiltRendererMaterial {
    Standard(Box<StandardMaterial>),
    Surface(Box<SurfaceMaterial>),
}

#[derive(Resource, Default, Debug, Clone)]
pub struct SurfaceDiagnostics(pub Vec<String>);

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SurfaceEvaluatorRecord {
    pub shader_id: u32,
    pub evaluator_source: String,
    pub revision: u64,
    pub alpha_mode: shared::MaterialAlphaMode,
}

/// Active, validated evaluator generation consumed by both raster and Solari render worlds.
#[derive(Resource, ExtractResource, Default, Debug, Clone)]
pub struct SurfaceEvaluatorRegistry {
    pub evaluators: HashMap<u32, SurfaceEvaluatorRecord>,
}

pub struct SurfaceMaterialPlugin;

impl Plugin for SurfaceMaterialPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins(MaterialPlugin::<SurfaceMaterial>::default())
            .init_resource::<SurfaceMaterialCache>()
            .init_resource::<SurfaceDiagnostics>()
            .init_resource::<SurfaceEvaluatorRegistry>()
            .add_systems(Update, sync_surface_material_bindings);
    }
}

#[allow(clippy::too_many_arguments, clippy::type_complexity)]
fn sync_surface_material_bindings(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut shaders: ResMut<Assets<Shader>>,
    mut standard_materials: ResMut<Assets<StandardMaterial>>,
    mut surface_materials: ResMut<Assets<SurfaceMaterial>>,
    mut cache: ResMut<SurfaceMaterialCache>,
    mut diagnostics: ResMut<SurfaceDiagnostics>,
    mut evaluator_registry: ResMut<SurfaceEvaluatorRegistry>,
    bindings: Query<(
        Entity,
        &HydratedRendererMaterialBinding,
        Option<&MeshMaterial3d<StandardMaterial>>,
        Option<&MeshMaterial3d<SurfaceMaterial>>,
    )>,
) {
    for (entity, binding, standard_handle, surface_handle) in &bindings {
        let Some(reference) = binding.effective_material.as_ref() else {
            continue;
        };
        let Some(path) = reference.0.source_path.as_deref() else {
            continue;
        };
        let dependencies = cache
            .dependencies
            .get(reference)
            .cloned()
            .unwrap_or_else(|| vec![path.to_string()]);
        let revision = dependency_revision(&dependencies);
        if cache.failed_revisions.get(reference) == Some(&revision) {
            continue;
        }
        if cache.standard_only.get(reference) == Some(&revision) {
            if let Some(handle) = cache.standard_handles.get(reference).cloned() {
                if standard_handle.is_none_or(|current| current.0 != handle)
                    || surface_handle.is_some()
                {
                    commands
                        .entity(entity)
                        .remove::<MeshMaterial3d<SurfaceMaterial>>()
                        .insert(MeshMaterial3d(handle));
                }
            }
            continue;
        }
        let stale = cache.revisions.get(reference) != Some(&revision);
        let handle = if !stale {
            cache.handles.get(reference).cloned()
        } else {
            None
        };
        let handle = match handle {
            Some(handle) => handle,
            None => match build_surface_material(
                reference,
                path,
                &asset_server,
                &mut shaders,
                &mut standard_materials,
                standard_handle,
                &mut evaluator_registry,
            ) {
                Ok(BuiltRendererMaterial::Surface(material)) => {
                    let material = *material;
                    cache.standard_only.remove(reference);
                    cache.standard_handles.remove(reference);
                    cache.failed_revisions.remove(reference);
                    if let Some(existing) = cache.handles.get(reference).cloned() {
                        if let Some(mut slot) = surface_materials.get_mut(&existing) {
                            *slot = material;
                        }
                        existing
                    } else {
                        let handle = surface_materials.add(material);
                        cache.handles.insert(reference.clone(), handle.clone());
                        handle
                    }
                }
                Ok(BuiltRendererMaterial::Standard(material)) => {
                    let material = *material;
                    // Plain Material assets stay on Bevy's StandardMaterial path, while still
                    // sharing one live-updated handle across every renderer slot.
                    let handle = standard_handle
                        .map(|handle| handle.0.clone())
                        .or_else(|| cache.standard_handles.get(reference).cloned())
                        .unwrap_or_else(|| standard_materials.add(material.clone()));
                    if let Some(mut slot) = standard_materials.get_mut(&handle) {
                        *slot = material;
                    }
                    cache.handles.remove(reference);
                    cache.revisions.remove(reference);
                    cache
                        .standard_handles
                        .insert(reference.clone(), handle.clone());
                    let next_dependencies =
                        material_dependency_paths(path).unwrap_or_else(|_| vec![path.to_string()]);
                    let next_revision = dependency_revision(&next_dependencies);
                    cache
                        .dependencies
                        .insert(reference.clone(), next_dependencies);
                    cache.standard_only.insert(reference.clone(), next_revision);
                    cache.failed_revisions.remove(reference);
                    evaluator_registry
                        .evaluators
                        .remove(&stable_shader_id(reference));
                    if standard_handle.is_none_or(|current| current.0 != handle)
                        || surface_handle.is_some()
                    {
                        commands
                            .entity(entity)
                            .remove::<MeshMaterial3d<SurfaceMaterial>>()
                            .insert(MeshMaterial3d(handle));
                    }
                    continue;
                }
                Err(error) => {
                    cache.failed_revisions.insert(reference.clone(), revision);
                    diagnostics.0.push(format!(
                        "material {} could not build Surface ABI: {error}",
                        reference.0.label
                    ));
                    continue;
                }
            },
        };
        let next_dependencies =
            material_dependency_paths(path).unwrap_or_else(|_| vec![path.to_string()]);
        let next_revision = dependency_revision(&next_dependencies);
        cache
            .dependencies
            .insert(reference.clone(), next_dependencies);
        cache.revisions.insert(reference.clone(), next_revision);
        if surface_handle.is_none_or(|current| current.0 != handle) {
            commands
                .entity(entity)
                .remove::<MeshMaterial3d<StandardMaterial>>()
                .insert(MeshMaterial3d(handle));
        }
    }
}

fn build_surface_material(
    reference: &MaterialRef,
    path: &str,
    asset_server: &AssetServer,
    shaders: &mut Assets<Shader>,
    standard_materials: &mut Assets<StandardMaterial>,
    existing_standard: Option<&MeshMaterial3d<StandardMaterial>>,
    evaluator_registry: &mut SurfaceEvaluatorRegistry,
) -> Result<BuiltRendererMaterial, String> {
    let (mut asset, instance) = match MaterialAsset::load_from_path(path) {
        Ok(asset) => (asset, None),
        Err(_) => {
            let instance = MaterialInstanceAsset::load_from_path(path)?;
            if instance.schema_version != MATERIAL_INSTANCE_SCHEMA_VERSION {
                return Err(format!(
                    "unsupported material-instance schema {}",
                    instance.schema_version
                ));
            }
            let base_path = instance
                .base
                .0
                .source_path
                .as_deref()
                .ok_or_else(|| "material instance base has no loadable path".to_string())?;
            (MaterialAsset::load_from_path(base_path)?, Some(instance))
        }
    };
    if let Some(instance) = instance.as_ref() {
        instance.apply_to(&mut asset.material);
    }
    let schema_path = asset
        .shader_ref
        .as_ref()
        .and_then(|reference| reference.source_path.as_deref())
        .or(asset.material.shader.schema_path.as_deref())
        .or(asset
            .shader
            .as_deref()
            .filter(|path| path.ends_with(".ron")));
    let schema = schema_path
        .map(ShaderSchemaAsset::load_from_path)
        .transpose()?;
    let evaluator_source = schema
        .as_ref()
        .and_then(|schema| schema.wgsl_path.as_deref())
        .map(fs::read_to_string)
        .transpose()
        .map_err(|error| format!("could not read surface WGSL: {error}"))?;

    let mut base = existing_standard
        .and_then(|handle| standard_materials.get(&handle.0).cloned())
        .unwrap_or_else(|| material_from_desc(asset_server, &asset.material));
    base.alpha_mode = match asset.render_state.alpha_mode {
        shared::MaterialAlphaMode::Opaque => AlphaMode::Opaque,
        shared::MaterialAlphaMode::Cutout => AlphaMode::Mask(asset.render_state.alpha_cutoff),
    };
    base.cull_mode =
        (!asset.render_state.double_sided).then_some(bevy::render::render_resource::Face::Back);

    let Some(evaluator) = evaluator_source.as_deref() else {
        return Ok(BuiltRendererMaterial::Standard(Box::new(base)));
    };
    validate_surface_evaluator(evaluator)?;
    let runtime_shader_id = stable_shader_id(reference);
    evaluator_registry.evaluators.insert(
        runtime_shader_id,
        SurfaceEvaluatorRecord {
            shader_id: runtime_shader_id,
            evaluator_source: evaluator.to_string(),
            revision: evaluator_revision(evaluator),
            alpha_mode: asset.render_state.alpha_mode,
        },
    );
    let virtual_path = format!("generated://surface/{}.wgsl", reference.0.asset_id);
    let shader_uuid = shader_uuid_for_material(reference);
    let shader_id = AssetId::<Shader>::Uuid { uuid: shader_uuid };
    shaders
        .insert(
            shader_id,
            Shader::from_wgsl(compose_surface_shader(evaluator), virtual_path),
        )
        .map_err(|error| format!("could not replace generated surface shader: {error}"))?;
    let shader = Handle::Uuid(shader_uuid, Default::default());

    let mut extension = SurfaceExtension {
        shader,
        ..default()
    };
    extension.uniform.alpha_cutoff = asset.render_state.alpha_cutoff;
    extension.uniform.shader_id = runtime_shader_id;
    if let Some(schema) = schema.as_ref() {
        pack_schema_values(
            &mut extension,
            schema,
            &asset.material,
            instance.as_ref(),
            asset_server,
        )?;
    }
    Ok(BuiltRendererMaterial::Surface(Box::new(ExtendedMaterial {
        base,
        extension,
    })))
}

fn material_dependency_paths(path: &str) -> Result<Vec<String>, String> {
    let mut dependencies = vec![path.to_string()];
    let asset = match MaterialAsset::load_from_path(path) {
        Ok(asset) => asset,
        Err(_) => {
            let instance = MaterialInstanceAsset::load_from_path(path)?;
            let base_path = instance
                .base
                .0
                .source_path
                .as_deref()
                .ok_or_else(|| "material instance base has no loadable path".to_string())?;
            dependencies.push(base_path.to_string());
            MaterialAsset::load_from_path(base_path)?
        }
    };
    let schema_path = asset
        .shader_ref
        .as_ref()
        .and_then(|reference| reference.source_path.as_deref())
        .or(asset.material.shader.schema_path.as_deref())
        .or(asset
            .shader
            .as_deref()
            .filter(|path| path.ends_with(".ron")));
    if let Some(schema_path) = schema_path {
        dependencies.push(schema_path.to_string());
        if let Some(wgsl_path) = ShaderSchemaAsset::load_from_path(schema_path)?.wgsl_path {
            dependencies.push(wgsl_path);
        }
    }
    dependencies.sort();
    dependencies.dedup();
    Ok(dependencies)
}

fn dependency_revision(paths: &[String]) -> u64 {
    let mut hash = 0xcbf29ce484222325u64;
    for path in paths {
        for byte in path.bytes() {
            hash ^= u64::from(byte);
            hash = hash.wrapping_mul(0x100000001b3);
        }
        match fs::metadata(path) {
            Ok(metadata) => {
                for byte in metadata.len().to_le_bytes() {
                    hash ^= u64::from(byte);
                    hash = hash.wrapping_mul(0x100000001b3);
                }
                if let Ok(modified) = metadata.modified() {
                    if let Ok(duration) = modified.duration_since(std::time::UNIX_EPOCH) {
                        for byte in duration.as_nanos().to_le_bytes() {
                            hash ^= u64::from(byte);
                            hash = hash.wrapping_mul(0x100000001b3);
                        }
                    }
                }
            }
            Err(_) => {
                hash ^= u64::MAX;
                hash = hash.wrapping_mul(0x100000001b3);
            }
        }
    }
    hash
}

fn stable_shader_id(reference: &MaterialRef) -> u32 {
    let mut hash = 0x811c9dc5u32;
    for byte in reference.0.asset_id.bytes() {
        hash ^= u32::from(byte);
        hash = hash.wrapping_mul(0x01000193);
    }
    hash
}

fn evaluator_revision(source: &str) -> u64 {
    let mut hash = 0xcbf29ce484222325u64;
    for byte in source.bytes() {
        hash ^= u64::from(byte);
        hash = hash.wrapping_mul(0x100000001b3);
    }
    hash
}

fn shader_uuid_for_material(reference: &MaterialRef) -> uuid::Uuid {
    if let Ok(source) = uuid::Uuid::parse_str(&reference.0.asset_id) {
        let mixed = source.as_u128() ^ 0x7f8c_4e90_f9ab_4f28_9b44_47c1_b2d5_5ac1u128;
        return uuid::Uuid::from_u128(mixed);
    }
    let mut bytes = [0u8; 16];
    for (index, byte) in reference.0.asset_id.bytes().enumerate() {
        bytes[index % 16] = bytes[index % 16].wrapping_mul(31).wrapping_add(byte);
    }
    uuid::Uuid::from_bytes(bytes)
}

fn pack_schema_values(
    extension: &mut SurfaceExtension,
    schema: &ShaderSchemaAsset,
    material: &shared::MaterialDesc,
    instance: Option<&MaterialInstanceAsset>,
    asset_server: &AssetServer,
) -> Result<(), String> {
    let mut numeric_index = 0usize;
    let mut texture_index = 0usize;
    for property in &schema.parameters {
        if matches!(property.property_type, ShaderPropertyType::Texture) {
            if texture_index >= SURFACE_TEXTURE_SLOTS {
                return Err(format!(
                    "surface schema exceeds {SURFACE_TEXTURE_SLOTS} textures"
                ));
            }
            let binding = instance
                .and_then(|instance| {
                    instance
                        .textures
                        .iter()
                        .find(|value| value.name == property.name)
                })
                .or_else(|| {
                    material
                        .textures
                        .iter()
                        .find(|value| value.name == property.name)
                });
            let handle = binding
                .and_then(|binding| binding.texture.as_ref())
                .and_then(|reference| reference.source_path.as_deref())
                .map(asset_server_path)
                .map(|path| asset_server.load(path));
            extension.set_texture(texture_index, handle);
            texture_index += 1;
            continue;
        }
        if numeric_index >= SURFACE_PARAMETER_LANES {
            return Err(format!(
                "surface schema exceeds {SURFACE_PARAMETER_LANES} parameter lanes"
            ));
        }
        let value = instance
            .and_then(|instance| {
                instance
                    .parameters
                    .iter()
                    .find(|value| value.name == property.name)
            })
            .or_else(|| {
                material
                    .parameters
                    .iter()
                    .find(|value| value.name == property.name)
            })
            .or_else(|| {
                schema
                    .default_values
                    .iter()
                    .find(|value| value.name == property.name)
            });
        extension.uniform.params[numeric_index] = value
            .map(|value| parameter_lane(&value.value, &property.property_type))
            .transpose()?
            .unwrap_or(Vec4::ZERO);
        numeric_index += 1;
    }
    Ok(())
}

fn parameter_lane(
    value: &MaterialParameterValue,
    property_type: &ShaderPropertyType,
) -> Result<Vec4, String> {
    match (value, property_type) {
        (MaterialParameterValue::Bool(value), ShaderPropertyType::Bool) => {
            Ok(Vec4::new(u32::from(*value) as f32, 0.0, 0.0, 0.0))
        }
        (MaterialParameterValue::Float(value), ShaderPropertyType::Float { .. }) => {
            Ok(Vec4::new(*value, 0.0, 0.0, 0.0))
        }
        (MaterialParameterValue::Vec2(value), ShaderPropertyType::Vec2) => {
            Ok(Vec4::new(value.x, value.y, 0.0, 0.0))
        }
        (MaterialParameterValue::Vec3(value), ShaderPropertyType::Vec3) => Ok(value.extend(0.0)),
        (MaterialParameterValue::Color(value), ShaderPropertyType::Color) => {
            Ok(Vec4::new(value.r, value.g, value.b, value.a))
        }
        (MaterialParameterValue::Enum(value), ShaderPropertyType::Enum { options }) => {
            let index = options
                .iter()
                .position(|option| option == value)
                .ok_or_else(|| format!("enum value `{value}` is not declared by the schema"))?;
            Ok(Vec4::new(index as f32, 0.0, 0.0, 0.0))
        }
        _ => Err("material value does not match shader schema type".into()),
    }
}

pub fn validate_surface_evaluator(source: &str) -> Result<(), String> {
    if !source.contains("fn evaluate(") {
        return Err("surface WGSL must define `fn evaluate(`".into());
    }
    for forbidden in [
        "@group",
        "@binding",
        "@vertex",
        "@fragment",
        "@compute",
        "var<",
        "rayQuery",
        "dpdx",
        "dpdy",
        "fwidth",
        "discard",
        "Barrier",
        "subgroup",
        "textureStore",
        "atomic",
    ] {
        if source.contains(forbidden) {
            return Err(format!(
                "surface WGSL uses forbidden construct `{forbidden}`"
            ));
        }
    }
    let validation_source = format!("{EVALUATOR_VALIDATION_PRELUDE}\n{source}");
    let module = naga::front::wgsl::parse_str(&validation_source)
        .map_err(|error| format!("surface WGSL syntax error: {error}"))?;
    naga::valid::Validator::new(
        naga::valid::ValidationFlags::all(),
        naga::valid::Capabilities::empty(),
    )
    .validate(&module)
    .map_err(|error| format!("surface WGSL validation error: {error}"))?;
    Ok(())
}

const EVALUATOR_VALIDATION_PRELUDE: &str = r#"
struct SurfaceInput {
    uv0: vec2<f32>,
    world_position: vec3<f32>,
    world_normal: vec3<f32>,
}
struct SurfaceParams { lanes: array<vec4<f32>, 16>, }
struct SurfaceSamples { values: array<vec4<f32>, 8>, }
struct Surface {
    base_color: vec4<f32>,
    normal_ts: vec3<f32>,
    emissive: vec3<f32>,
    metallic: f32,
    perceptual_roughness: f32,
    reflectance: f32,
    occlusion: f32,
    model: u32,
}
fn surface_default() -> Surface {
    var surface: Surface;
    surface.base_color = vec4<f32>(1.0);
    surface.normal_ts = vec3<f32>(0.0, 0.0, 1.0);
    surface.emissive = vec3<f32>(0.0);
    surface.metallic = 0.0;
    surface.perceptual_roughness = 0.5;
    surface.reflectance = 0.5;
    surface.occlusion = 1.0;
    surface.model = 0u;
    return surface;
}
"#;

pub fn compose_surface_shader(evaluator: &str) -> String {
    format!(
        "{}\n{}\n{}",
        include_str!("surface_header.wgsl"),
        evaluator,
        include_str!("surface_footer.wgsl")
    )
}

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

    #[test]
    fn rejects_resource_bindings_and_missing_entry() {
        assert!(validate_surface_evaluator("fn nope() {}").is_err());
        assert!(validate_surface_evaluator(
            "@group(0) @binding(0) var t: texture_2d<f32>; fn evaluate() {}"
        )
        .is_err());
        for compute_incompatible in [
            "dpdx(input.uv0.x)",
            "dpdy(input.uv0.y)",
            "fwidth(input.uv0.x)",
            "discard",
            "workgroupBarrier()",
            "subgroupAdd(1u)",
        ] {
            let source = format!(
                "fn evaluate(input: SurfaceInput, params: SurfaceParams, samples: SurfaceSamples) -> Surface {{ let invalid = {compute_incompatible}; return surface_default(); }}"
            );
            assert!(
                validate_surface_evaluator(&source).is_err(),
                "accepted compute-incompatible evaluator: {compute_incompatible}"
            );
        }
        assert!(validate_surface_evaluator(
            "fn evaluate(input: SurfaceInput, params: SurfaceParams, samples: SurfaceSamples) -> Surface { return missing_symbol; }"
        )
        .is_err());
    }

    #[test]
    fn accepts_surface_only_module_and_composes_wrapper() {
        let source = "fn evaluate(input: SurfaceInput, params: SurfaceParams, samples: SurfaceSamples) -> Surface { return surface_default(); }";
        validate_surface_evaluator(source).unwrap();
        let composed = compose_surface_shader(source);
        assert!(composed.contains(source));
        assert!(composed.contains("@fragment"));
    }

    #[test]
    fn abi_limits_are_stable() {
        assert_eq!(SURFACE_PARAMETER_LANES, 16);
        assert_eq!(SURFACE_TEXTURE_SLOTS, 8);
        assert_eq!(std::mem::size_of::<SurfaceUniform>(), 400);
    }
}
