//! Reusable invariant assertions for modal tools and editor operators.

use std::fmt::Debug;

use bevy::prelude::*;
use shared::LevelObject;

use crate::history::{apply_command_redo, apply_command_undo, EditorHistory};
use crate::operators::{ActiveOperator, OperatorPhase};
use crate::scene_io::SceneIo;

#[derive(Debug)]
pub(crate) struct OperatorInvariantHarness {
    undo_depth: usize,
    dirty: bool,
    authored_count: usize,
}

impl OperatorInvariantHarness {
    pub(crate) fn capture(world: &mut World) -> Self {
        Self {
            undo_depth: world
                .get_resource::<EditorHistory>()
                .map(EditorHistory::undo_depth)
                .unwrap_or_default(),
            dirty: world
                .get_resource::<SceneIo>()
                .is_some_and(|scene_io| scene_io.dirty),
            authored_count: authored_count(world),
        }
    }

    pub(crate) fn assert_committed(
        &self,
        world: &mut World,
        undo_delta: usize,
        authored_delta: isize,
    ) {
        self.assert_phase(world, OperatorPhase::Committed);
        assert_eq!(
            world.resource::<EditorHistory>().undo_depth(),
            self.undo_depth + undo_delta,
            "committed operator must create the expected number of undo groups"
        );
        if undo_delta > 0 {
            assert!(
                world.resource::<SceneIo>().dirty,
                "a committed authored mutation must mark the scene dirty"
            );
        }
        assert_eq!(
            authored_count(world) as isize,
            self.authored_count as isize + authored_delta,
            "committed operator changed an unexpected number of authored entities"
        );
    }

    pub(crate) fn assert_canceled(&self, world: &mut World) {
        self.assert_phase(world, OperatorPhase::Canceled);
        assert_eq!(
            world.resource::<EditorHistory>().undo_depth(),
            self.undo_depth,
            "canceled operator must not create undo history"
        );
        assert_eq!(
            world.resource::<SceneIo>().dirty,
            self.dirty,
            "canceled operator must preserve the prior dirty state"
        );
        assert_eq!(
            authored_count(world),
            self.authored_count,
            "canceled operator must restore authored entity state"
        );
    }

    pub(crate) fn assert_blocked(&self, world: &mut World) {
        self.assert_phase(world, OperatorPhase::Blocked);
        assert_eq!(
            world.resource::<EditorHistory>().undo_depth(),
            self.undo_depth
        );
        assert_eq!(world.resource::<SceneIo>().dirty, self.dirty);
        assert_eq!(authored_count(world), self.authored_count);
    }

    pub(crate) fn assert_no_helpers<T: Component>(&self, world: &mut World) {
        let mut query = world.query_filtered::<Entity, With<T>>();
        assert_eq!(
            query.iter(world).count(),
            0,
            "operator left preview/helper entities behind"
        );
    }

    fn assert_phase(&self, world: &World, expected: OperatorPhase) {
        let phase = world
            .resource::<ActiveOperator>()
            .status
            .as_ref()
            .map(|status| status.phase.clone());
        assert_eq!(phase, Some(expected), "operator reported the wrong phase");
    }
}

pub(crate) fn assert_undo_redo_round_trip<T>(
    world: &mut World,
    initial: T,
    committed: T,
    mut read: impl FnMut(&mut World) -> T,
) where
    T: Debug + PartialEq,
{
    assert_eq!(
        read(world),
        committed,
        "commit state did not match expectation"
    );
    apply_command_undo(world);
    assert_eq!(
        read(world),
        initial,
        "undo did not restore the initial state"
    );
    apply_command_redo(world);
    assert_eq!(
        read(world),
        committed,
        "redo did not restore the committed state"
    );
}

fn authored_count(world: &mut World) -> usize {
    let mut query = world.query_filtered::<Entity, With<LevelObject>>();
    query.iter(world).count()
}
