//! Modal terrain material-weight painting with exact cancel and grouped history.

use bevy::prelude::*;
use bevy_egui::EguiContexts;
use shared::{TerrainDesc, AUTHORING_COMPONENT_TERRAIN, COMPONENT_TERRAIN_DESC};

use super::terrain_sculpt::{active_scene_ray, smooth_falloff, terrain_ray_hit, TerrainHit};
use crate::camera::EditorCamera;
use crate::history::{reflected_component_transaction, EditorHistory};
use crate::operators::{ActiveOperator, OperatorPhase, OperatorStatus};
use crate::scene_io::SceneIo;
use crate::selection::{SelectedEntity, ViewportClick};
use crate::state::scene_tools_active;
use crate::ui::UiState;
use crate::viewport::ViewportDisplayMode;

const MIN_RADIUS: f32 = 0.25;
const MAX_RADIUS: f32 = 128.0;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TerrainPaintMode {
    #[default]
    Paint,
    Erase,
}

impl TerrainPaintMode {
    pub fn label(self) -> &'static str {
        match self {
            Self::Paint => "Paint",
            Self::Erase => "Erase",
        }
    }
}

#[derive(Debug, Clone)]
struct TerrainPaintStroke {
    entity: Entity,
    original: TerrainDesc,
    last_local: Vec3,
}

#[derive(Resource, Debug, Clone)]
pub struct TerrainPaintState {
    pub active: bool,
    pub mode: TerrainPaintMode,
    pub active_layer: usize,
    pub radius: f32,
    pub strength: f32,
    hover: Option<TerrainHit>,
    stroke: Option<TerrainPaintStroke>,
    target: Option<Entity>,
}

impl Default for TerrainPaintState {
    fn default() -> Self {
        Self {
            active: false,
            mode: TerrainPaintMode::Paint,
            active_layer: 1,
            radius: 4.0,
            strength: 0.35,
            hover: None,
            stroke: None,
            target: None,
        }
    }
}

impl TerrainPaintState {
    pub fn start(&mut self, target: Entity, layer_count: usize) {
        self.active = true;
        self.target = Some(target);
        self.active_layer = self.active_layer.min(layer_count.saturating_sub(1));
    }

    pub fn stop(&mut self) {
        self.active = false;
        self.hover = None;
        self.target = None;
    }

    pub fn is_stroking(&self) -> bool {
        self.stroke.is_some()
    }

    pub fn clamp_settings(&mut self) {
        self.radius = self.radius.clamp(MIN_RADIUS, MAX_RADIUS);
        self.strength = self.strength.clamp(0.01, 1.0);
    }
}

pub struct TerrainPaintPlugin;

impl Plugin for TerrainPaintPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<TerrainPaintState>().add_systems(
            Update,
            (terrain_paint_input, draw_terrain_paint_preview)
                .chain()
                .run_if(scene_tools_active),
        );
    }
}

#[allow(clippy::too_many_arguments)]
fn terrain_paint_input(
    mut commands: Commands,
    mut state: ResMut<TerrainPaintState>,
    mut selected: ResMut<SelectedEntity>,
    keys: Res<ButtonInput<KeyCode>>,
    buttons: Res<ButtonInput<MouseButton>>,
    mut contexts: EguiContexts,
    ui_state: Res<UiState>,
    display: Res<ViewportDisplayMode>,
    cameras: Query<(&Camera, &GlobalTransform), With<EditorCamera>>,
    mut terrains: Query<(&GlobalTransform, &mut TerrainDesc)>,
    mut viewport_click: ResMut<ViewportClick>,
    mut scene_io: ResMut<SceneIo>,
    mut active_operator: ResMut<ActiveOperator>,
) -> Result {
    if !state.active {
        return Ok(());
    }
    viewport_click.0 = None;
    state.clamp_settings();
    let Some(entity) = state.target.or(selected.0) else {
        stop_tool_with_rollback(
            &mut state,
            &mut terrains,
            &mut scene_io,
            &mut active_operator,
            OperatorPhase::Blocked,
            "Terrain paint stopped: target is unavailable",
            "Target unavailable",
        );
        return Ok(());
    };
    if selected.0 != Some(entity) {
        selected.0 = Some(entity);
    }
    if display.clean_game_view {
        stop_tool_with_rollback(
            &mut state,
            &mut terrains,
            &mut scene_io,
            &mut active_operator,
            OperatorPhase::Canceled,
            "Terrain paint stopped for clean game view",
            "Clean game view enabled; weights restored",
        );
        return Ok(());
    }
    let ctx = contexts.ctx_mut()?;
    let cancel_requested =
        keys.just_pressed(KeyCode::Escape) || buttons.just_pressed(MouseButton::Right);
    if cancel_requested {
        if state.is_stroking() {
            cancel_active_stroke(
                &mut state,
                &mut terrains,
                &mut scene_io,
                &mut active_operator,
            );
        } else {
            state.stop();
            scene_io.status = "Terrain paint tool closed".into();
            set_status(&mut active_operator, OperatorPhase::Canceled, "Tool closed");
        }
        return Ok(());
    }
    let Ok((global, mut terrain)) = terrains.get_mut(entity) else {
        stop_tool_with_rollback(
            &mut state,
            &mut terrains,
            &mut scene_io,
            &mut active_operator,
            OperatorPhase::Blocked,
            "Terrain paint stopped: target no longer exists",
            "Target no longer exists",
        );
        return Ok(());
    };
    let layer_count = terrain.material_layers.len();
    if layer_count == 0 {
        if let Some(stroke) = state.stroke.take() {
            *terrain = stroke.original;
        }
        state.stop();
        scene_io.status = "Add a terrain material layer before painting".into();
        set_status(
            &mut active_operator,
            OperatorPhase::Blocked,
            "Add a material layer before painting",
        );
        return Ok(());
    }
    state.active_layer = state.active_layer.min(layer_count - 1);
    state.hover = ui_state
        .viewport_pointer_pos
        .and_then(|pointer| active_scene_ray(&cameras, pointer, ui_state.viewport_rect))
        .and_then(|ray| terrain_ray_hit(entity, &terrain, global, ray));

    if buttons.just_pressed(MouseButton::Left) && !ctx.egui_wants_pointer_input() {
        if let Some(hit) = state.hover.filter(|hit| hit.entity == entity) {
            let original = terrain.clone();
            terrain.ensure_material_weights();
            apply_weight_dab(
                &mut terrain,
                hit.local,
                state.radius,
                state.strength,
                state.active_layer,
                state.mode,
            );
            state.stroke = Some(TerrainPaintStroke {
                entity,
                original,
                last_local: hit.local,
            });
            set_status(
                &mut active_operator,
                OperatorPhase::Preview,
                format!(
                    "{} layer {} in progress",
                    state.mode.label(),
                    state.active_layer + 1
                ),
            );
        }
    } else if buttons.pressed(MouseButton::Left) {
        let radius = state.radius;
        let strength = state.strength;
        let layer = state.active_layer;
        let mode = state.mode;
        if let (Some(hit), Some(stroke)) = (state.hover, state.stroke.as_mut()) {
            if hit.entity == stroke.entity {
                let step = (radius * 0.2).max(terrain.sample_spacing * 0.25);
                let delta = hit.local - stroke.last_local;
                let distance = Vec2::new(delta.x, delta.z).length();
                if distance >= step {
                    let count = (distance / step).floor() as usize;
                    for index in 1..=count {
                        let point = stroke.last_local + delta * (index as f32 / count as f32);
                        apply_weight_dab(&mut terrain, point, radius, strength, layer, mode);
                    }
                    stroke.last_local = hit.local;
                }
            }
        }
    }

    if buttons.just_released(MouseButton::Left) {
        if let Some(stroke) = state.stroke.take() {
            let final_terrain = terrain.clone();
            let mode = state.mode;
            commands.queue(move |world: &mut World| {
                finish_paint_stroke(world, stroke, final_terrain, mode);
            });
        }
    }
    Ok(())
}

fn commit_paint_stroke(
    world: &mut World,
    stroke: TerrainPaintStroke,
    final_terrain: TerrainDesc,
) -> Result<(), String> {
    let original = stroke.original;
    if let Ok(mut actor) = world.get_entity_mut(stroke.entity) {
        actor.insert(original.clone());
    }
    let result = reflected_component_transaction(
        world,
        stroke.entity,
        "Paint Terrain Material",
        AUTHORING_COMPONENT_TERRAIN,
        COMPONENT_TERRAIN_DESC,
        move |world, entity| {
            world.entity_mut(entity).insert(final_terrain);
            Ok(())
        },
    );
    if result.is_err() {
        if let Ok(mut actor) = world.get_entity_mut(stroke.entity) {
            actor.insert(original);
        }
    }
    result
}

fn finish_paint_stroke(
    world: &mut World,
    stroke: TerrainPaintStroke,
    final_terrain: TerrainDesc,
    mode: TerrainPaintMode,
) {
    let undo_depth = world.resource::<EditorHistory>().undo_depth();
    match commit_paint_stroke(world, stroke, final_terrain) {
        Ok(()) if world.resource::<EditorHistory>().undo_depth() > undo_depth => {
            world.resource_mut::<SceneIo>().status =
                format!("{} terrain material weights committed", mode.label());
            set_status(
                &mut world.resource_mut::<ActiveOperator>(),
                OperatorPhase::Committed,
                format!("{} weights; Ctrl+Z to undo", mode.label()),
            );
        }
        Ok(()) => {
            world.resource_mut::<SceneIo>().status =
                format!("{} terrain stroke made no changes", mode.label());
            set_status(
                &mut world.resource_mut::<ActiveOperator>(),
                OperatorPhase::Canceled,
                format!("{} stroke made no changes", mode.label()),
            );
        }
        Err(error) => {
            world.resource_mut::<SceneIo>().status =
                format!("Terrain paint commit failed: {error}");
            set_status(
                &mut world.resource_mut::<ActiveOperator>(),
                OperatorPhase::Blocked,
                format!("Commit failed: {error}"),
            );
        }
    }
}

fn cancel_active_stroke(
    state: &mut TerrainPaintState,
    terrains: &mut Query<(&GlobalTransform, &mut TerrainDesc)>,
    scene_io: &mut SceneIo,
    active_operator: &mut ActiveOperator,
) {
    cancel_stroke(state, terrains);
    scene_io.status = "Terrain paint stroke canceled".into();
    set_status(active_operator, OperatorPhase::Canceled, "Weights restored");
}

fn stop_tool_with_rollback(
    state: &mut TerrainPaintState,
    terrains: &mut Query<(&GlobalTransform, &mut TerrainDesc)>,
    scene_io: &mut SceneIo,
    active_operator: &mut ActiveOperator,
    phase: OperatorPhase,
    message: &str,
    hint: &str,
) {
    cancel_stroke(state, terrains);
    state.stop();
    scene_io.status = message.to_string();
    set_status(active_operator, phase, hint);
}

fn cancel_stroke(
    state: &mut TerrainPaintState,
    terrains: &mut Query<(&GlobalTransform, &mut TerrainDesc)>,
) {
    let Some(stroke) = state.stroke.take() else {
        return;
    };
    if let Ok((_, mut terrain)) = terrains.get_mut(stroke.entity) {
        *terrain = stroke.original;
    }
}

fn set_status(active: &mut ActiveOperator, phase: OperatorPhase, hint: impl Into<String>) {
    active.status = Some(OperatorStatus {
        id: "terrain.paint".into(),
        label: "Terrain Paint".into(),
        phase,
        hint: hint.into(),
        warnings: Vec::new(),
    });
}

fn apply_weight_dab(
    terrain: &mut TerrainDesc,
    center: Vec3,
    radius: f32,
    strength: f32,
    active_layer: usize,
    mode: TerrainPaintMode,
) {
    terrain.ensure_material_weights();
    let layer_count = terrain.material_layers.len().clamp(1, 4);
    if active_layer >= layer_count {
        return;
    }
    let radius = radius.max(MIN_RADIUS);
    let half_extent = (terrain.resolution - 1) as f32 * terrain.sample_spacing * 0.5;
    for z in 0..terrain.resolution {
        for x in 0..terrain.resolution {
            let sample = Vec2::new(
                x as f32 * terrain.sample_spacing - half_extent,
                z as f32 * terrain.sample_spacing - half_extent,
            );
            let distance = sample.distance(Vec2::new(center.x, center.z));
            if distance > radius {
                continue;
            }
            let amount = strength.clamp(0.0, 1.0) * smooth_falloff(distance / radius);
            let index = (z * terrain.resolution + x) as usize;
            terrain.material_weights[index] = adjust_weights(
                terrain.material_weights[index],
                active_layer,
                layer_count,
                amount,
                mode,
            );
        }
    }
}

fn adjust_weights(
    weights: [u8; 4],
    active_layer: usize,
    layer_count: usize,
    amount: f32,
    mode: TerrainPaintMode,
) -> [u8; 4] {
    let mut values = weights.map(|value| f32::from(value) / 255.0);
    for value in values.iter_mut().skip(layer_count) {
        *value = 0.0;
    }
    let current = values[active_layer];
    let target = match mode {
        TerrainPaintMode::Paint => current + (1.0 - current) * amount,
        TerrainPaintMode::Erase => current * (1.0 - amount),
    };
    let other_sum: f32 = values
        .iter()
        .take(layer_count)
        .enumerate()
        .filter(|(index, _)| *index != active_layer)
        .map(|(_, value)| *value)
        .sum();
    if other_sum <= f32::EPSILON {
        if layer_count == 1 {
            values[active_layer] = 1.0;
        } else {
            values.fill(0.0);
            values[active_layer] = target;
            let fallback = if active_layer == 0 { 1 } else { 0 };
            values[fallback] = 1.0 - target;
        }
    } else {
        let scale = (1.0 - target) / other_sum;
        for (index, value) in values.iter_mut().take(layer_count).enumerate() {
            if index != active_layer {
                *value *= scale;
            }
        }
        values[active_layer] = target;
    }
    quantize_weights(values, layer_count)
}

fn quantize_weights(values: [f32; 4], layer_count: usize) -> [u8; 4] {
    let mut result = [0_u8; 4];
    let mut fractions = [(0_usize, 0.0_f32); 4];
    let mut assigned = 0_u16;
    for index in 0..layer_count {
        let scaled = values[index].clamp(0.0, 1.0) * 255.0;
        let floor = scaled.floor() as u8;
        result[index] = floor;
        assigned += u16::from(floor);
        fractions[index] = (index, scaled - f32::from(floor));
    }
    fractions[..layer_count].sort_by(|a, b| b.1.total_cmp(&a.1));
    for offset in 0..usize::from(255_u16 - assigned) {
        result[fractions[offset % layer_count].0] += 1;
    }
    result
}

fn draw_terrain_paint_preview(state: Res<TerrainPaintState>, mut gizmos: Gizmos) {
    if !state.active {
        return;
    }
    let Some(hit) = state.hover else {
        return;
    };
    let color = match state.mode {
        TerrainPaintMode::Paint => Color::srgba(0.22, 0.78, 1.0, 0.95),
        TerrainPaintMode::Erase => Color::srgba(1.0, 0.64, 0.22, 0.95),
    };
    let rotation = hit.world_rotation * Quat::from_rotation_x(std::f32::consts::FRAC_PI_2);
    gizmos
        .circle(
            Isometry3d::new(hit.world + Vec3::Y * 0.035, rotation),
            state.radius,
            color,
        )
        .resolution(48);
    gizmos.sphere(
        Isometry3d::from_translation(hit.world + Vec3::Y * 0.04),
        0.06,
        color,
    );
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::history::EditorHistory;
    use crate::operators::test_harness::{assert_undo_redo_round_trip, OperatorInvariantHarness};
    use shared::{ActorKind, LevelObject};

    #[test]
    fn paint_and_erase_keep_weights_exactly_normalized() {
        let painted = adjust_weights([255, 0, 0, 0], 1, 2, 0.5, TerrainPaintMode::Paint);
        assert_eq!(painted.iter().copied().map(u16::from).sum::<u16>(), 255);
        assert!(painted[1] > 0);
        let erased = adjust_weights(painted, 1, 2, 1.0, TerrainPaintMode::Erase);
        assert_eq!(erased, [255, 0, 0, 0]);
    }

    #[test]
    fn dab_only_changes_samples_inside_footprint() {
        let mut terrain = TerrainDesc::flat(5);
        terrain.material_layers = vec![Default::default(), Default::default()];
        apply_weight_dab(
            &mut terrain,
            Vec3::ZERO,
            1.1,
            1.0,
            1,
            TerrainPaintMode::Paint,
        );
        assert!(terrain.material_weights[12][1] > 0);
        assert_eq!(terrain.material_weights[0], [255, 0, 0, 0]);
        assert!(terrain.validate().is_ok());
    }

    #[test]
    fn many_dabs_commit_as_one_undoable_paint_stroke() {
        let mut app = App::new();
        app.register_type::<ActorKind>()
            .register_type::<TerrainDesc>()
            .register_type::<shared::TerrainMaterialLayer>()
            .register_type::<shared::EditorAssetRef>();
        let world = app.world_mut();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        world.init_resource::<TerrainPaintState>();
        let mut original = TerrainDesc::flat(9);
        original.material_layers = vec![Default::default(), Default::default()];
        let entity = world
            .spawn((LevelObject, ActorKind::Terrain, original.clone()))
            .id();
        let mut final_terrain = original.clone();
        for x in [-2.0, 0.0, 2.0] {
            apply_weight_dab(
                &mut final_terrain,
                Vec3::new(x, 0.0, 0.0),
                2.5,
                0.5,
                1,
                TerrainPaintMode::Paint,
            );
        }
        world.entity_mut(entity).insert(final_terrain.clone());
        let stroke = TerrainPaintStroke {
            entity,
            original: original.clone(),
            last_local: Vec3::ZERO,
        };
        {
            let mut state = world.resource_mut::<TerrainPaintState>();
            state.start(entity, 2);
            state.stroke = Some(stroke);
        }
        let stroke = world
            .resource_mut::<TerrainPaintState>()
            .stroke
            .take()
            .unwrap();
        let harness = OperatorInvariantHarness::capture(world);

        finish_paint_stroke(
            world,
            stroke,
            final_terrain.clone(),
            TerrainPaintMode::Paint,
        );

        harness.assert_committed(world, 1, 0);
        harness.assert_status(world, "terrain.paint", OperatorPhase::Committed);
        assert_eq!(world.get::<TerrainDesc>(entity), Some(&final_terrain));
        let state = world.resource::<TerrainPaintState>();
        assert!(state.active);
        assert_eq!(state.target, Some(entity));
        assert!(!state.is_stroking());
        assert_undo_redo_round_trip(world, original, final_terrain, |world| {
            world.get::<TerrainDesc>(entity).unwrap().clone()
        });
    }

    #[test]
    fn no_op_paint_release_terminates_without_dirtying_or_history() {
        let mut app = App::new();
        app.register_type::<ActorKind>()
            .register_type::<TerrainDesc>()
            .register_type::<shared::TerrainMaterialLayer>()
            .register_type::<shared::EditorAssetRef>();
        let world = app.world_mut();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        let mut original = TerrainDesc::flat(5);
        original.material_layers = vec![Default::default(), Default::default()];
        original.ensure_material_weights();
        let entity = world
            .spawn((LevelObject, ActorKind::Terrain, original.clone()))
            .id();
        let harness = OperatorInvariantHarness::capture(world);

        finish_paint_stroke(
            world,
            TerrainPaintStroke {
                entity,
                original: original.clone(),
                last_local: Vec3::ZERO,
            },
            original.clone(),
            TerrainPaintMode::Erase,
        );

        harness.assert_canceled(world);
        harness.assert_status(world, "terrain.paint", OperatorPhase::Canceled);
        assert_eq!(world.get::<TerrainDesc>(entity), Some(&original));
    }

    #[test]
    fn commit_failure_restores_preview_and_reports_blocked_without_history() {
        let mut app = App::new();
        let world = app.world_mut();
        world.init_resource::<ActiveOperator>();
        world.init_resource::<EditorHistory>();
        world.init_resource::<SceneIo>();
        let mut original = TerrainDesc::flat(5);
        original.material_layers = vec![Default::default(), Default::default()];
        let mut preview = original.clone();
        apply_weight_dab(
            &mut preview,
            Vec3::ZERO,
            3.0,
            0.8,
            1,
            TerrainPaintMode::Paint,
        );
        let entity = world
            .spawn((LevelObject, ActorKind::Terrain, preview.clone()))
            .id();
        let mut hierarchy = crate::ui::hierarchy_state::HierarchyPanelState::default();
        hierarchy.locked.insert(entity);
        world.insert_resource(hierarchy);
        let harness = OperatorInvariantHarness::capture(world);

        finish_paint_stroke(
            world,
            TerrainPaintStroke {
                entity,
                original: original.clone(),
                last_local: Vec3::ZERO,
            },
            preview,
            TerrainPaintMode::Paint,
        );

        harness.assert_blocked(world);
        harness.assert_status(world, "terrain.paint", OperatorPhase::Blocked);
        assert_eq!(world.get::<TerrainDesc>(entity), Some(&original));
        assert!(world
            .resource::<SceneIo>()
            .status
            .starts_with("Terrain paint commit failed:"));
    }

    #[test]
    fn cancel_restores_implicit_pre_stroke_weights() {
        fn cancel_once(
            mut state: ResMut<TerrainPaintState>,
            mut terrains: Query<(&GlobalTransform, &mut TerrainDesc)>,
            mut scene_io: ResMut<SceneIo>,
            mut active_operator: ResMut<ActiveOperator>,
        ) {
            cancel_active_stroke(
                &mut state,
                &mut terrains,
                &mut scene_io,
                &mut active_operator,
            );
        }

        let mut app = App::new();
        app.init_resource::<ActiveOperator>()
            .init_resource::<EditorHistory>()
            .init_resource::<SceneIo>();
        let mut original = TerrainDesc::flat(5);
        original.material_layers = vec![Default::default(), Default::default()];
        let mut preview = original.clone();
        apply_weight_dab(
            &mut preview,
            Vec3::ZERO,
            3.0,
            0.8,
            1,
            TerrainPaintMode::Paint,
        );
        let entity = app
            .world_mut()
            .spawn((LevelObject, GlobalTransform::default(), preview))
            .id();
        let mut state = TerrainPaintState::default();
        state.start(entity, 2);
        state.stroke = Some(TerrainPaintStroke {
            entity,
            original: original.clone(),
            last_local: Vec3::ZERO,
        });
        app.insert_resource(state).add_systems(Update, cancel_once);
        let harness = OperatorInvariantHarness::capture(app.world_mut());

        app.update();
        harness.assert_canceled(app.world_mut());
        harness.assert_status(app.world(), "terrain.paint", OperatorPhase::Canceled);
        assert_eq!(app.world().get::<TerrainDesc>(entity), Some(&original));
        let state = app.world().resource::<TerrainPaintState>();
        assert!(state.active);
        assert_eq!(state.target, Some(entity));
        assert!(!state.is_stroking());
    }

    #[test]
    fn clean_view_interruption_restores_weights_and_terminates_tool() {
        fn interrupt_once(
            mut state: ResMut<TerrainPaintState>,
            mut terrains: Query<(&GlobalTransform, &mut TerrainDesc)>,
            mut scene_io: ResMut<SceneIo>,
            mut active_operator: ResMut<ActiveOperator>,
        ) {
            stop_tool_with_rollback(
                &mut state,
                &mut terrains,
                &mut scene_io,
                &mut active_operator,
                OperatorPhase::Canceled,
                "Terrain paint stopped for clean game view",
                "Clean game view enabled; weights restored",
            );
        }

        let mut app = App::new();
        app.init_resource::<ActiveOperator>()
            .init_resource::<EditorHistory>()
            .init_resource::<SceneIo>();
        let mut original = TerrainDesc::flat(5);
        original.material_layers = vec![Default::default(), Default::default()];
        let mut preview = original.clone();
        apply_weight_dab(
            &mut preview,
            Vec3::ZERO,
            3.0,
            0.8,
            1,
            TerrainPaintMode::Paint,
        );
        let entity = app
            .world_mut()
            .spawn((LevelObject, GlobalTransform::default(), preview))
            .id();
        let mut state = TerrainPaintState::default();
        state.start(entity, 2);
        state.stroke = Some(TerrainPaintStroke {
            entity,
            original: original.clone(),
            last_local: Vec3::ZERO,
        });
        app.insert_resource(state)
            .add_systems(Update, interrupt_once);
        let harness = OperatorInvariantHarness::capture(app.world_mut());

        app.update();

        harness.assert_canceled(app.world_mut());
        harness.assert_status(app.world(), "terrain.paint", OperatorPhase::Canceled);
        assert_eq!(app.world().get::<TerrainDesc>(entity), Some(&original));
        assert!(!app.world().resource::<TerrainPaintState>().active);
    }
}
