use super::*;
use crate::surface_abi::surface_evaluator_owns_fragment;
use bevy::ecs::world::CommandQueue;

fn live_material_app() -> App {
    let mut app = App::new();
    app.add_plugins((MinimalPlugins, AssetPlugin::default()))
        .init_resource::<Assets<Shader>>()
        .init_resource::<Assets<StandardMaterial>>()
        .init_resource::<Assets<SurfaceMaterial>>()
        .init_resource::<SurfaceMaterialCache>()
        .init_resource::<LiveMaterialDocumentOverlay>()
        .init_resource::<DefaultGridMaterialHandles>()
        .init_resource::<shared::ProjectContentDefaults>()
        .init_resource::<SurfaceDiagnostics>()
        .init_resource::<SurfaceEvaluatorRegistry>()
        .add_systems(Update, sync_surface_material_bindings);
    app
}

fn authored_standard_material(label: &str, color: shared::ColorDesc) -> MaterialAsset {
    MaterialAsset {
        schema_version: shared::MATERIAL_ASSET_SCHEMA_VERSION,
        label: label.into(),
        shader: shared::ShaderRefDesc::default(),
        shader_ref: None,
        render_state: shared::MaterialRenderState::default(),
        inputs: shared::MaterialInputSet::from_material_desc(&MaterialDesc {
            base_color: color,
            roughness: 0.4,
            ..Default::default()
        }),
        provenance: None,
    }
}

#[test]
fn stale_generated_terrain_binding_is_nonfatal_during_scene_switch() {
    let mut world = World::new();
    let entity = world.spawn_empty().id();
    let mut queue = CommandQueue::default();
    let mut commands = Commands::new(&mut queue, &world);
    queue_terrain_material_binding(&mut commands, entity, Handle::default());

    world.entity_mut(entity).despawn();
    queue.apply(&mut world);

    assert!(world.get_entity(entity).is_err());
}

#[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 embedded_default_grid_is_uv_independent_and_surface_valid() {
    validate_surface_evaluator(DEFAULT_GRID_EVALUATOR).unwrap();
    assert!(DEFAULT_GRID_EVALUATOR.contains("world_position"));
    assert!(DEFAULT_GRID_EVALUATOR.contains("0.5"));
    assert!(DEFAULT_GRID_EVALUATOR.contains("4.0"));
    assert!(!DEFAULT_GRID_EVALUATOR.contains("uv0"));
}

#[test]
fn assigned_standard_material_replaces_emergency_values() {
    let path = std::env::temp_dir().join(format!(
        "blacksite-assigned-material-{}-{}.material.ron",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ));
    let authored_color = shared::ColorDesc::srgb(0.25, 0.0, 1.0);
    let asset = MaterialAsset {
        schema_version: shared::MATERIAL_ASSET_SCHEMA_VERSION,
        label: "Purple Chrome".into(),
        shader: shared::ShaderRefDesc::default(),
        shader_ref: None,
        render_state: shared::MaterialRenderState::default(),
        inputs: shared::MaterialInputSet::from_material_desc(&MaterialDesc {
            base_color: authored_color,
            metallic: 0.93,
            roughness: 0.17,
            ..Default::default()
        }),
        provenance: None,
    };
    std::fs::write(
        &path,
        ron::ser::to_string_pretty(&asset, ron::ser::PrettyConfig::default()).unwrap(),
    )
    .unwrap();

    let mut app = App::new();
    app.add_plugins((MinimalPlugins, AssetPlugin::default()))
        .init_resource::<Assets<Shader>>()
        .init_resource::<Assets<StandardMaterial>>()
        .init_resource::<Assets<SurfaceMaterial>>()
        .init_resource::<SurfaceMaterialCache>()
        .init_resource::<LiveMaterialDocumentOverlay>()
        .init_resource::<DefaultGridMaterialHandles>()
        .init_resource::<shared::ProjectContentDefaults>()
        .init_resource::<SurfaceDiagnostics>()
        .init_resource::<SurfaceEvaluatorRegistry>()
        .add_systems(Update, sync_surface_material_bindings);
    let emergency = app
        .world_mut()
        .resource_mut::<Assets<StandardMaterial>>()
        .add(StandardMaterial {
            base_color: Color::srgb(0.46, 0.46, 0.46),
            metallic: 0.0,
            perceptual_roughness: 0.86,
            ..Default::default()
        });
    let reference = MaterialRef::new(
        shared::EditorAssetRef::new("purple-chrome", "material:source", "Purple Chrome")
            .with_source_path(path.to_string_lossy()),
    );
    let draw = app
        .world_mut()
        .spawn((
            HydratedMaterialSlotBinding {
                owner: Entity::PLACEHOLDER,
                slot_id: shared::ComponentInstanceId::new("slot:primitive:surface"),
                selection: HydratedMaterialSelection::Project {
                    reference,
                    layer: shared::HydratedMaterialLayer::Actor,
                },
            },
            MeshMaterial3d(emergency.clone()),
        ))
        .id();

    app.update();

    let resolved = app
        .world()
        .get::<MeshMaterial3d<StandardMaterial>>(draw)
        .unwrap();
    assert_ne!(
        resolved.0, emergency,
        "assigned Materials must detach from the singleton emergency handle"
    );
    let materials = app.world().resource::<Assets<StandardMaterial>>();
    let material = materials.get(&resolved.0).unwrap();
    assert_eq!(material.base_color, authored_color.to_color());
    assert_eq!(material.metallic, 0.93);
    assert_eq!(material.perceptual_roughness, 0.17);
    let emergency_material = materials.get(&emergency).unwrap();
    assert_eq!(emergency_material.base_color, Color::srgb(0.46, 0.46, 0.46));
    assert_eq!(emergency_material.metallic, 0.0);
    assert_eq!(emergency_material.perceptual_roughness, 0.86);
    assert!(app.world().resource::<SurfaceDiagnostics>().0.is_empty());

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

#[test]
fn live_material_edits_persist_across_frames_and_reuse_the_handle() {
    let mut app = live_material_app();
    let reference = MaterialRef::new(
        shared::EditorAssetRef::new("live-material", "material:source", "Live Material")
            .with_source_path("assets/materials/live_material.ron"),
    );
    let first_color = shared::ColorDesc::srgb(0.8, 0.1, 0.05);
    app.world_mut()
        .resource_mut::<LiveMaterialDocumentOverlay>()
        .update_material(
            reference.0.asset_id.clone(),
            1,
            authored_standard_material("Live Material", first_color),
        );
    let emergency = app
        .world_mut()
        .resource_mut::<Assets<StandardMaterial>>()
        .add(StandardMaterial::default());
    let draw = app
        .world_mut()
        .spawn((
            HydratedMaterialSlotBinding {
                owner: Entity::PLACEHOLDER,
                slot_id: shared::ComponentInstanceId::new("slot:primitive:surface"),
                selection: HydratedMaterialSelection::Project {
                    reference: reference.clone(),
                    layer: shared::HydratedMaterialLayer::Actor,
                },
            },
            MeshMaterial3d(emergency),
        ))
        .id();

    app.update();
    let handle = app
        .world()
        .get::<MeshMaterial3d<StandardMaterial>>(draw)
        .unwrap()
        .0
        .clone();
    assert_eq!(
        app.world()
            .resource::<Assets<StandardMaterial>>()
            .get(&handle)
            .unwrap()
            .base_color,
        first_color.to_color()
    );

    let second_color = shared::ColorDesc::srgb(0.05, 0.15, 0.9);
    app.world_mut()
        .resource_mut::<LiveMaterialDocumentOverlay>()
        .update_material(
            reference.0.asset_id.clone(),
            2,
            authored_standard_material("Live Material", second_color),
        );
    app.update();
    app.update();

    assert_eq!(
        app.world()
            .get::<MeshMaterial3d<StandardMaterial>>(draw)
            .unwrap()
            .0,
        handle,
        "ordinary live edits must mutate the cached handle instead of rehydrating geometry"
    );
    assert_eq!(
        app.world()
            .resource::<Assets<StandardMaterial>>()
            .get(&handle)
            .unwrap()
            .base_color,
        second_color.to_color(),
        "disk-backed state must not overwrite the live document on a later frame"
    );
}

#[test]
fn live_base_material_changes_flow_through_direct_instances() {
    let mut app = live_material_app();
    let base_reference = MaterialRef::new(
        shared::EditorAssetRef::new("live-base", "material:source", "Live Base")
            .with_source_path("assets/materials/live_base.ron"),
    );
    let instance_reference = MaterialRef::new(
        shared::EditorAssetRef::new("live-instance", "material:instance", "Live Instance")
            .with_source_path("assets/materials/live_instance.ron"),
    );
    let first_color = shared::ColorDesc::srgb(0.7, 0.2, 0.1);
    {
        let mut overlay = app
            .world_mut()
            .resource_mut::<LiveMaterialDocumentOverlay>();
        overlay.update_material(
            base_reference.0.asset_id.clone(),
            1,
            authored_standard_material("Live Base", first_color),
        );
        overlay.update_instance(
            instance_reference.0.asset_id.clone(),
            1,
            MaterialInstanceAsset {
                schema_version: shared::MATERIAL_INSTANCE_SCHEMA_VERSION,
                label: "Live Instance".into(),
                base: base_reference.clone(),
                overrides: shared::MaterialInputSet {
                    values: vec![shared::MaterialParameter {
                        name: "metallic".into(),
                        value: MaterialParameterValue::Float(0.75),
                    }],
                    textures: Vec::new(),
                },
            },
        );
    }
    let emergency = app
        .world_mut()
        .resource_mut::<Assets<StandardMaterial>>()
        .add(StandardMaterial::default());
    let draw = app
        .world_mut()
        .spawn((
            HydratedMaterialSlotBinding {
                owner: Entity::PLACEHOLDER,
                slot_id: shared::ComponentInstanceId::new("slot:body"),
                selection: HydratedMaterialSelection::Project {
                    reference: instance_reference,
                    layer: shared::HydratedMaterialLayer::Actor,
                },
            },
            MeshMaterial3d(emergency),
        ))
        .id();
    app.update();
    let handle = app
        .world()
        .get::<MeshMaterial3d<StandardMaterial>>(draw)
        .unwrap()
        .0
        .clone();

    let second_color = shared::ColorDesc::srgb(0.1, 0.25, 0.85);
    app.world_mut()
        .resource_mut::<LiveMaterialDocumentOverlay>()
        .update_material(
            base_reference.0.asset_id.clone(),
            2,
            authored_standard_material("Live Base", second_color),
        );
    app.update();
    app.update();

    let materials = app.world().resource::<Assets<StandardMaterial>>();
    let resolved = materials.get(&handle).unwrap();
    assert_eq!(resolved.base_color, second_color.to_color());
    assert_eq!(resolved.metallic, 0.75);
    assert_eq!(
        app.world()
            .get::<MeshMaterial3d<StandardMaterial>>(draw)
            .unwrap()
            .0,
        handle
    );
}

#[test]
fn property_block_revision_is_stable_and_changes_with_values() {
    let mut block = MaterialPropertyBlock {
        slot_id: shared::ComponentInstanceId::new("slot:body"),
        parameters: vec![shared::MaterialParameter {
            name: "roughness".into(),
            value: MaterialParameterValue::Float(0.25),
        }],
        textures: Vec::new(),
    };
    let first = property_block_revision(&block, None, None, 7);
    assert_eq!(first, property_block_revision(&block, None, None, 7));
    block.parameters[0].value = MaterialParameterValue::Float(0.8);
    assert_ne!(first, property_block_revision(&block, None, None, 7));
    assert_ne!(first, property_block_revision(&block, None, None, 8));
}

#[test]
fn invalid_standard_property_blocks_are_rejected_before_mutation() {
    let mut block = MaterialPropertyBlock {
        slot_id: shared::ComponentInstanceId::new("slot:body"),
        parameters: vec![shared::MaterialParameter {
            name: "unsupported".into(),
            value: MaterialParameterValue::Float(0.25),
        }],
        textures: Vec::new(),
    };

    assert!(validate_standard_property_block(&block, false).is_err());
    assert!(validate_standard_property_block(&block, true).is_ok());
    block.parameters[0] = shared::MaterialParameter {
        name: "roughness".into(),
        value: MaterialParameterValue::Float(1.25),
    };
    assert!(validate_standard_property_block(&block, false).is_err());
    block.parameters[0].value = MaterialParameterValue::Color(shared::ColorDesc::default());
    assert!(validate_standard_property_block(&block, false).is_err());
}

#[test]
fn surface_property_block_overrides_existing_instance_value() {
    let property = shared::MaterialParameter {
        name: "tint_strength".into(),
        value: MaterialParameterValue::Float(0.9),
    };
    let block = MaterialPropertyBlock {
        slot_id: shared::ComponentInstanceId::new("slot:body"),
        parameters: vec![property.clone()],
        textures: Vec::new(),
    };
    let schema = ShaderSchemaAsset {
        schema_version: shared::SURFACE_SHADER_SCHEMA_VERSION,
        label: "Tint".into(),
        kind: shared::MaterialShaderKind::Custom,
        wgsl_path: None,
        schema: shared::MaterialInputSchema {
            groups: vec![shared::MaterialInputGroupDesc {
                id: "surface".into(),
                display_name: "Surface".into(),
                ..Default::default()
            }],
            inputs: vec![shared::ShaderPropertyDesc {
                name: "tint_strength".into(),
                display_name: "Tint strength".into(),
                group: "surface".into(),
                order: 0,
                property_type: ShaderPropertyType::Float {
                    min: Some(0.0),
                    max: Some(1.0),
                },
                default_value: Some(MaterialParameterValue::Float(0.0)),
                texture: None,
                tooltip: String::new(),
                advanced: false,
                presentation: Default::default(),
            }],
        },
    };
    validate_standard_property_block(&block, true).unwrap();
    validate_surface_property_block(Some(&schema), &block).unwrap();

    let mut asset = MaterialAsset {
        schema_version: shared::MATERIAL_ASSET_SCHEMA_VERSION,
        label: "Base".into(),
        shader: shared::ShaderRefDesc::default(),
        shader_ref: None,
        render_state: shared::MaterialRenderState::default(),
        inputs: shared::MaterialInputSet::from_material_desc(&MaterialDesc {
            parameters: vec![shared::MaterialParameter {
                name: "tint_strength".into(),
                value: MaterialParameterValue::Float(0.1),
            }],
            ..Default::default()
        }),
        provenance: None,
    };
    let mut instance = Some(MaterialInstanceAsset {
        schema_version: MATERIAL_INSTANCE_SCHEMA_VERSION,
        label: "Variant".into(),
        base: MaterialRef::new(shared::EditorAssetRef::new(
            "base-id",
            "material:base",
            "Base",
        )),
        overrides: shared::MaterialInputSet {
            values: vec![shared::MaterialParameter {
                name: "tint_strength".into(),
                value: MaterialParameterValue::Float(0.4),
            }],
            textures: Vec::new(),
        },
    });

    merge_surface_property_block(&mut asset, &mut instance, &block);

    assert_eq!(
        instance.unwrap().overrides.values,
        vec![property],
        "runtime block must be stronger than the selected Material Instance"
    );
    assert_eq!(
        asset
            .inputs
            .values
            .iter()
            .find(|value| value.name == "tint_strength")
            .unwrap()
            .value,
        MaterialParameterValue::Float(0.1),
        "promotion/runtime overrides must not mutate the shared base asset"
    );
}

#[test]
fn standard_property_blocks_are_owner_local_and_reuse_cached_handles() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
        .add_plugins(AssetPlugin::default())
        .init_resource::<Assets<StandardMaterial>>()
        .init_resource::<Assets<SurfaceMaterial>>()
        .init_resource::<SurfaceMaterialCache>()
        .init_resource::<MaterialPropertyBlockCache>()
        .init_resource::<SurfaceDiagnostics>()
        .add_systems(Update, apply_material_property_blocks);
    let base = app
        .world_mut()
        .resource_mut::<Assets<StandardMaterial>>()
        .add(StandardMaterial {
            perceptual_roughness: 0.5,
            ..Default::default()
        });
    let slot = shared::ComponentInstanceId::new("slot:body");
    let spawn_owner = |world: &mut World, roughness: f32| {
        world
            .spawn(MaterialPropertyBlocks {
                slots: vec![MaterialPropertyBlock {
                    slot_id: slot.clone(),
                    parameters: vec![shared::MaterialParameter {
                        name: "roughness".into(),
                        value: MaterialParameterValue::Float(roughness),
                    }],
                    textures: Vec::new(),
                }],
            })
            .id()
    };
    let owner_a = spawn_owner(app.world_mut(), 0.2);
    let owner_b = spawn_owner(app.world_mut(), 0.8);
    let spawn_draw = |world: &mut World, owner| {
        world
            .spawn((
                HydratedMaterialSlotBinding {
                    owner,
                    slot_id: slot.clone(),
                    selection: HydratedMaterialSelection::Inherit,
                },
                MeshMaterial3d(base.clone()),
            ))
            .id()
    };
    let draw_a = spawn_draw(app.world_mut(), owner_a);
    let draw_b = spawn_draw(app.world_mut(), owner_b);

    app.update();

    let handle_a = app
        .world()
        .get::<MeshMaterial3d<StandardMaterial>>(draw_a)
        .unwrap()
        .0
        .clone();
    let handle_b = app
        .world()
        .get::<MeshMaterial3d<StandardMaterial>>(draw_b)
        .unwrap()
        .0
        .clone();
    assert_ne!(handle_a, handle_b);
    assert_ne!(handle_a, base);
    assert_ne!(handle_b, base);
    let materials = app.world().resource::<Assets<StandardMaterial>>();
    assert_eq!(materials.get(&base).unwrap().perceptual_roughness, 0.5);
    assert_eq!(materials.get(&handle_a).unwrap().perceptual_roughness, 0.2);
    assert_eq!(materials.get(&handle_b).unwrap().perceptual_roughness, 0.8);
    assert_eq!(
        app.world()
            .resource::<MaterialPropertyBlockCache>()
            .entries
            .len(),
        2
    );
    let material_count = materials.len();

    app.update();

    assert_eq!(
        app.world()
            .get::<MeshMaterial3d<StandardMaterial>>(draw_a)
            .unwrap()
            .0,
        handle_a
    );
    assert_eq!(
        app.world()
            .get::<MeshMaterial3d<StandardMaterial>>(draw_b)
            .unwrap()
            .0,
        handle_b
    );
    assert_eq!(
        app.world().resource::<Assets<StandardMaterial>>().len(),
        material_count,
        "steady property-block application must not grow material handles"
    );
}

#[test]
fn invalid_property_block_keeps_base_visible_with_one_diagnostic() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
        .add_plugins(AssetPlugin::default())
        .init_resource::<Assets<StandardMaterial>>()
        .init_resource::<Assets<SurfaceMaterial>>()
        .init_resource::<SurfaceMaterialCache>()
        .init_resource::<MaterialPropertyBlockCache>()
        .init_resource::<SurfaceDiagnostics>()
        .add_systems(Update, apply_material_property_blocks);
    let base = app
        .world_mut()
        .resource_mut::<Assets<StandardMaterial>>()
        .add(StandardMaterial::default());
    let slot_id = shared::ComponentInstanceId::new("slot:body");
    let owner = app
        .world_mut()
        .spawn(MaterialPropertyBlocks {
            slots: vec![MaterialPropertyBlock {
                slot_id: slot_id.clone(),
                parameters: vec![shared::MaterialParameter {
                    name: "unsupported".into(),
                    value: MaterialParameterValue::Float(0.5),
                }],
                textures: Vec::new(),
            }],
        })
        .id();
    let draw = app
        .world_mut()
        .spawn((
            HydratedMaterialSlotBinding {
                owner,
                slot_id,
                selection: HydratedMaterialSelection::Inherit,
            },
            MeshMaterial3d(base.clone()),
        ))
        .id();

    app.update();
    app.update();

    assert_eq!(
        app.world()
            .get::<MeshMaterial3d<StandardMaterial>>(draw)
            .unwrap()
            .0,
        base
    );
    let diagnostics = &app.world().resource::<SurfaceDiagnostics>().0;
    assert_eq!(diagnostics.len(), 1);
    assert!(diagnostics[0].contains("unsupported"));
}

#[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);
}

#[test]
fn material_extensions_leave_non_deferred_prepasses_to_standard_material() {
    assert!(matches!(
        <SurfaceExtension as MaterialExtension>::prepass_fragment_shader(),
        ShaderRef::Default
    ));
    assert!(matches!(
        <TerrainLayerExtension as MaterialExtension>::prepass_fragment_shader(),
        ShaderRef::Default
    ));
    assert!(!surface_evaluator_owns_fragment(&[
        "PREPASS_PIPELINE".into(),
        "NORMAL_PREPASS".into(),
    ]));
    assert!(surface_evaluator_owns_fragment(&[
        "PREPASS_PIPELINE".into(),
        "DEFERRED_PREPASS".into(),
    ]));
    assert!(surface_evaluator_owns_fragment(&[]));
}

#[test]
fn terrain_layer_builder_packs_project_material_values() {
    let mut app = App::new();
    app.add_plugins((MinimalPlugins, AssetPlugin::default()));
    let asset_server = app.world().resource::<AssetServer>();
    let materials_root =
        std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/materials");
    let layers = [
        TerrainMaterialLayer {
            material: Some(
                shared::EditorAssetRef::new("concrete", "material:source", "Concrete")
                    .with_source_path(materials_root.join("concrete.ron").display().to_string()),
            ),
            uv_scale: 6.0,
        },
        TerrainMaterialLayer {
            material: Some(
                shared::EditorAssetRef::new("tint", "material:source", "Surface Tint")
                    .with_source_path(
                        materials_root
                            .join("surface_tint.ron")
                            .display()
                            .to_string(),
                    ),
            ),
            uv_scale: 10.0,
        },
    ];
    let mut diagnostics = Vec::new();
    let live_documents = LiveMaterialDocumentOverlay::default();
    let material =
        build_terrain_layer_material(asset_server, &layers, &live_documents, &mut diagnostics);

    assert!(diagnostics.is_empty(), "{diagnostics:?}");
    assert_eq!(material.extension.uniform.uv_scales.x, 6.0);
    assert_eq!(material.extension.uniform.uv_scales.y, 10.0);
    assert_eq!(material.extension.uniform.properties[0].w, 1.0);
    assert_eq!(material.extension.uniform.properties[1].w, 1.0);
    assert_eq!(
        material.base.opaque_render_method,
        OpaqueRendererMethod::Forward
    );
    assert_ne!(
        material.extension.uniform.base_colors[0],
        material.extension.uniform.base_colors[1]
    );
}
