use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use serde::de::DeserializeOwned;
use serde_json::Value;

#[cfg(feature = "async")]
use std::future::Future;
#[cfg(feature = "async")]
use std::pin::Pin;

use crate::context::GlobalContext;
use crate::error::{Result, SdkError};
use crate::state::{State, StateContainer};

/// A boxed, pinned, Send future — the return type of async handlers.
#[cfg(feature = "async")]
pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;

// ---------------------------------------------------------------------------
// Callback types
// ---------------------------------------------------------------------------

/// Lifecycle callback: receives immutable state and optional global context.
type LifecycleHandler<S> = Box<dyn Fn(&S, Option<&GlobalContext>) + Send + Sync>;

/// Type-erased action handler: receives mutable state, optional raw JSON
/// payload, and optional global context.
type ActionHandlerFn<S> = Box<dyn Fn(&mut S, Option<&Value>, Option<&GlobalContext>) + Send + Sync>;

/// Error handler: receives the error context and returns whether the error was handled.
type ErrorHandler = Box<dyn Fn(&ErrorContext) -> ErrorResult + Send + Sync>;

// Async handler types (behind "async" feature)

/// Async lifecycle handler: takes owned state, returns owned state after `.await`.
#[cfg(feature = "async")]
type AsyncLifecycleHandler<S> =
    Box<dyn Fn(S, Option<Arc<GlobalContext>>) -> BoxFuture<S> + Send + Sync>;

/// Async type-erased action handler: takes owned state, optional raw JSON
/// payload, and optional global context. Returns owned state after `.await`.
#[cfg(feature = "async")]
type AsyncActionHandlerFn<S> =
    Box<dyn Fn(S, Option<Value>, Option<Arc<GlobalContext>>) -> BoxFuture<S> + Send + Sync>;

/// Context provided to error handlers.
pub struct ErrorContext {
    pub error: SdkError,
    pub action_name: Option<String>,
    pub lifecycle: Option<String>,
}

/// Result from an error handler.
pub struct ErrorResult {
    /// If true, the error is considered handled and won't propagate.
    pub handled: bool,
}

// ---------------------------------------------------------------------------
// ModuleDefinition
// ---------------------------------------------------------------------------

/// An immutable, built module definition.
///
/// Created via `ModuleBuilder::build()`. Contains all state, handlers, and
/// UI configuration needed to instantiate a running module.
pub struct ModuleDefinition<S: State> {
    pub(crate) name: String,
    pub(crate) initial_state: S,
    pub(crate) ui_source: Option<String>,
    #[allow(dead_code)]
    pub(crate) ui_file: Option<String>,
    pub(crate) action_handlers: HashMap<String, ActionHandlerFn<S>>,
    pub(crate) on_created: Option<LifecycleHandler<S>>,
    pub(crate) on_destroyed: Option<LifecycleHandler<S>>,
    #[allow(dead_code)]
    pub(crate) on_error: Option<ErrorHandler>,
    pub(crate) persist: bool,
    #[cfg(feature = "async")]
    pub(crate) async_action_handlers: HashMap<String, AsyncActionHandlerFn<S>>,
    #[cfg(feature = "async")]
    pub(crate) on_created_async: Option<AsyncLifecycleHandler<S>>,
    #[cfg(feature = "async")]
    pub(crate) on_destroyed_async: Option<AsyncLifecycleHandler<S>>,
}

impl<S: State> ModuleDefinition<S> {
    pub fn name(&self) -> &str {
        &self.name
    }

    pub fn action_names(&self) -> Vec<String> {
        self.action_handlers.keys().cloned().collect()
    }

    pub fn ui_source(&self) -> Option<&str> {
        self.ui_source.as_deref()
    }

    pub fn is_persistent(&self) -> bool {
        self.persist
    }
}

// ---------------------------------------------------------------------------
// ModuleBuilder (fluent API)
// ---------------------------------------------------------------------------

/// Fluent builder for constructing a `ModuleDefinition`.
///
/// # Example
///
/// ```rust,ignore
/// use hypen_server::prelude::*;
/// use serde::{Deserialize, Serialize};
///
/// #[derive(Clone, Default, Serialize, Deserialize)]
/// struct Counter { count: i32 }
///
/// #[derive(Deserialize)]
/// struct AddPayload { amount: i32 }
///
/// let module = ModuleBuilder::new("Counter")
///     .state(Counter { count: 0 })
///     .ui(r#"Column { Text("Count: ${state.count}") }"#)
///     .on_action::<()>("increment", |state, _, _ctx| {
///         state.count += 1;
///     })
///     .on_action::<AddPayload>("add", |state, payload, _ctx| {
///         state.count += payload.amount;
///     })
///     .build();
/// ```
pub struct ModuleBuilder<S: State> {
    name: String,
    initial_state: Option<S>,
    ui_source: Option<String>,
    ui_file: Option<String>,
    action_handlers: HashMap<String, ActionHandlerFn<S>>,
    on_created: Option<LifecycleHandler<S>>,
    on_destroyed: Option<LifecycleHandler<S>>,
    on_error: Option<ErrorHandler>,
    persist: bool,
    #[cfg(feature = "async")]
    async_action_handlers: HashMap<String, AsyncActionHandlerFn<S>>,
    #[cfg(feature = "async")]
    on_created_async: Option<AsyncLifecycleHandler<S>>,
    #[cfg(feature = "async")]
    on_destroyed_async: Option<AsyncLifecycleHandler<S>>,
}

impl<S: State> ModuleBuilder<S> {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            initial_state: None,
            ui_source: None,
            ui_file: None,
            action_handlers: HashMap::new(),
            on_created: None,
            on_destroyed: None,
            on_error: None,
            persist: false,
            #[cfg(feature = "async")]
            async_action_handlers: HashMap::new(),
            #[cfg(feature = "async")]
            on_created_async: None,
            #[cfg(feature = "async")]
            on_destroyed_async: None,
        }
    }

    /// Set the initial state for this module.
    pub fn state(mut self, initial: S) -> Self {
        self.initial_state = Some(initial);
        self
    }

    /// Set the Hypen DSL template as an inline string.
    ///
    /// ```rust,ignore
    /// .ui(r#"
    ///     Column {
    ///         Text("Count: ${state.count}")
    ///         Button("@actions.increment") { Text("+") }
    ///     }
    /// "#)
    /// ```
    pub fn ui(mut self, source: impl Into<String>) -> Self {
        self.ui_source = Some(source.into());
        self
    }

    /// Load the Hypen DSL template from a file path.
    ///
    /// The file will be read when the module is instantiated.
    pub fn ui_file(mut self, path: impl Into<String>) -> Self {
        self.ui_file = Some(path.into());
        self
    }

    /// Register an action handler with a typed payload.
    ///
    /// The type parameter `A` determines how the action payload is
    /// deserialized. Use `()` for actions that don't carry a payload.
    ///
    /// # Examples
    ///
    /// ```rust,ignore
    /// // No payload — use ()
    /// .on_action::<()>("increment", |state, _, _ctx| {
    ///     state.count += 1;
    /// })
    ///
    /// // Typed payload — any Deserialize type
    /// #[derive(Deserialize)]
    /// struct SetValue { value: i32 }
    ///
    /// .on_action::<SetValue>("set_value", |state, payload, _ctx| {
    ///     state.count = payload.value;
    /// })
    ///
    /// // Raw JSON access
    /// .on_action::<serde_json::Value>("raw", |state, raw, _ctx| {
    ///     if let Some(n) = raw.as_i64() { state.count = n as i32; }
    /// })
    /// ```
    pub fn on_action<A>(
        mut self,
        name: impl Into<String>,
        handler: impl Fn(&mut S, A, Option<&GlobalContext>) + Send + Sync + 'static,
    ) -> Self
    where
        A: DeserializeOwned + 'static,
    {
        let wrapped: ActionHandlerFn<S> = Box::new(move |state, raw, ctx| {
            let action = match raw {
                Some(v) => serde_json::from_value::<A>(v.clone()).ok(),
                None => serde_json::from_value::<A>(Value::Null).ok(),
            };
            if let Some(action) = action {
                handler(state, action, ctx);
            }
        });
        self.action_handlers.insert(name.into(), wrapped);
        self
    }

    /// Called when the module is first mounted.
    pub fn on_created<F>(mut self, handler: F) -> Self
    where
        F: Fn(&S, Option<&GlobalContext>) + Send + Sync + 'static,
    {
        self.on_created = Some(Box::new(handler));
        self
    }

    /// Called when the module is destroyed/unmounted.
    pub fn on_destroyed<F>(mut self, handler: F) -> Self
    where
        F: Fn(&S, Option<&GlobalContext>) + Send + Sync + 'static,
    {
        self.on_destroyed = Some(Box::new(handler));
        self
    }

    /// Register an error handler for this module.
    pub fn on_error<F>(mut self, handler: F) -> Self
    where
        F: Fn(&ErrorContext) -> ErrorResult + Send + Sync + 'static,
    {
        self.on_error = Some(Box::new(handler));
        self
    }

    /// Mark this module as persistent (survives route changes).
    pub fn persist(mut self) -> Self {
        self.persist = true;
        self
    }

    /// Register an async action handler with a typed payload.
    ///
    /// The handler takes **owned** state, performs async work, and returns
    /// the (possibly mutated) state. This avoids holding `&mut` across
    /// `.await` points.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// .on_action_async::<AddPayload>("add", |mut state, payload, _ctx| {
    ///     Box::pin(async move {
    ///         state.count += payload.amount;
    ///         state
    ///     })
    /// })
    /// ```
    #[cfg(feature = "async")]
    pub fn on_action_async<A>(
        mut self,
        name: impl Into<String>,
        handler: impl Fn(S, A, Option<Arc<GlobalContext>>) -> BoxFuture<S> + Send + Sync + 'static,
    ) -> Self
    where
        A: DeserializeOwned + Send + 'static,
    {
        let wrapped: AsyncActionHandlerFn<S> = Box::new(move |state, raw, ctx| {
            let action = match raw {
                Some(v) => serde_json::from_value::<A>(v).ok(),
                None => serde_json::from_value::<A>(Value::Null).ok(),
            };
            if let Some(action) = action {
                handler(state, action, ctx)
            } else {
                Box::pin(async move { state })
            }
        });
        self.async_action_handlers.insert(name.into(), wrapped);
        self
    }

    /// Register an async `on_created` lifecycle handler.
    ///
    /// Takes owned state and returns it after async work.
    ///
    /// ```rust,ignore
    /// .on_created_async(|mut state, _ctx| {
    ///     Box::pin(async move {
    ///         // fetch data, initialize, etc.
    ///         state
    ///     })
    /// })
    /// ```
    #[cfg(feature = "async")]
    pub fn on_created_async(
        mut self,
        handler: impl Fn(S, Option<Arc<GlobalContext>>) -> BoxFuture<S> + Send + Sync + 'static,
    ) -> Self {
        self.on_created_async = Some(Box::new(handler));
        self
    }

    /// Register an async `on_destroyed` lifecycle handler.
    ///
    /// Takes owned state and returns it after async cleanup.
    ///
    /// ```rust,ignore
    /// .on_destroyed_async(|state, _ctx| {
    ///     Box::pin(async move {
    ///         // cleanup, flush logs, etc.
    ///         state
    ///     })
    /// })
    /// ```
    #[cfg(feature = "async")]
    pub fn on_destroyed_async(
        mut self,
        handler: impl Fn(S, Option<Arc<GlobalContext>>) -> BoxFuture<S> + Send + Sync + 'static,
    ) -> Self {
        self.on_destroyed_async = Some(Box::new(handler));
        self
    }

    /// Consume the builder and produce an immutable `ModuleDefinition`.
    pub fn build(self) -> ModuleDefinition<S> {
        let initial_state = self
            .initial_state
            .expect("ModuleBuilder::state() must be called before build()");

        ModuleDefinition {
            name: self.name,
            initial_state,
            ui_source: self.ui_source,
            ui_file: self.ui_file,
            action_handlers: self.action_handlers,
            on_created: self.on_created,
            on_destroyed: self.on_destroyed,
            on_error: self.on_error,
            persist: self.persist,
            #[cfg(feature = "async")]
            async_action_handlers: self.async_action_handlers,
            #[cfg(feature = "async")]
            on_created_async: self.on_created_async,
            #[cfg(feature = "async")]
            on_destroyed_async: self.on_destroyed_async,
        }
    }
}

// ---------------------------------------------------------------------------
// ModuleInstance (running module)
// ---------------------------------------------------------------------------

/// A running module instance with live state.
///
/// Created from a `ModuleDefinition` when the module is mounted.
/// Wraps a `hypen_engine::Engine` and manages state synchronization.
pub struct ModuleInstance<S: State> {
    definition: Arc<ModuleDefinition<S>>,
    state: Mutex<StateContainer<S>>,
    engine: Mutex<hypen_engine::Engine>,
    mounted: Mutex<bool>,
    global_context: Option<Arc<GlobalContext>>,
}

impl<S: State> ModuleInstance<S> {
    /// Create a new module instance from a definition.
    pub fn new(
        definition: Arc<ModuleDefinition<S>>,
        global_context: Option<Arc<GlobalContext>>,
    ) -> Result<Self> {
        let state_container = StateContainer::new(definition.initial_state.clone())?;
        let mut engine = hypen_engine::Engine::new();

        // Set up the engine module metadata
        let module_meta = hypen_engine::lifecycle::Module::new(&definition.name)
            .with_actions(definition.action_names())
            .with_persist(definition.persist);

        let initial_json = state_container.to_json()?;
        let engine_module = hypen_engine::ModuleInstance::new(module_meta, initial_json);
        engine.set_module(engine_module);

        // Parse and load UI if provided
        if let Some(ref source) = definition.ui_source {
            Self::load_ui_source(&mut engine, source)?;
        } else if let Some(ref path) = definition.ui_file {
            let source = std::fs::read_to_string(path).map_err(|e| {
                SdkError::Component(format!("Failed to read UI file '{path}': {e}"))
            })?;
            Self::load_ui_source(&mut engine, &source)?;
        }

        Ok(Self {
            definition,
            state: Mutex::new(state_container),
            engine: Mutex::new(engine),
            mounted: Mutex::new(false),
            global_context,
        })
    }

    fn load_ui_source(engine: &mut hypen_engine::Engine, source: &str) -> Result<()> {
        let doc = hypen_parser::parse_document(source).map_err(|e| {
            SdkError::Engine(hypen_engine::EngineError::ParseError {
                source: source.chars().take(80).collect(),
                message: format!("{e:?}"),
            })
        })?;
        let component = doc
            .components
            .first()
            .ok_or_else(|| SdkError::Component("No component found in UI source".to_string()))?;
        let ir_node = hypen_engine::ast_to_ir_node(component);
        engine.render_ir_node(&ir_node);
        Ok(())
    }

    /// Mount the module (triggers `on_created`).
    pub fn mount(&self) {
        let mut mounted = self.mounted.lock().unwrap();
        if !*mounted {
            *mounted = true;
            if let Some(ref handler) = self.definition.on_created {
                let state = self.state.lock().unwrap();
                let ctx = self.global_context.as_deref();
                handler(state.get(), ctx);
            }
        }
    }

    /// Unmount the module (triggers `on_destroyed`).
    pub fn unmount(&self) {
        let mut mounted = self.mounted.lock().unwrap();
        if *mounted {
            if let Some(ref handler) = self.definition.on_destroyed {
                let state = self.state.lock().unwrap();
                let ctx = self.global_context.as_deref();
                handler(state.get(), ctx);
            }
            *mounted = false;
        }
    }

    /// Dispatch an action by name with an optional payload.
    pub fn dispatch_action(&self, name: impl Into<String>, payload: Option<Value>) -> Result<()> {
        let name = name.into();

        // Snapshot state before handler runs
        {
            let mut state = self.state.lock().unwrap();
            state.take_snapshot()?;
        }

        let ctx = self.global_context.as_deref();

        if let Some(handler) = self.definition.action_handlers.get(&name) {
            let mut state = self.state.lock().unwrap();
            handler(state.get_mut(), payload.as_ref(), ctx);
        } else {
            return Err(SdkError::Engine(hypen_engine::EngineError::ActionNotFound(
                name,
            )));
        }

        // Diff state and notify engine of changes
        self.sync_state_to_engine()?;

        Ok(())
    }

    /// Get a clone of the current state.
    pub fn get_state(&self) -> S {
        self.state.lock().unwrap().get().clone()
    }

    /// Get the current state as JSON.
    pub fn get_state_json(&self) -> Result<Value> {
        self.state.lock().unwrap().to_json()
    }

    /// Set the render callback for receiving patches.
    pub fn on_patches<F>(&self, callback: F)
    where
        F: Fn(&[hypen_engine::Patch]) + Send + Sync + 'static,
    {
        let mut engine = self.engine.lock().unwrap();
        engine.set_render_callback(callback);
    }

    /// Check if the module is currently mounted.
    pub fn is_mounted(&self) -> bool {
        *self.mounted.lock().unwrap()
    }

    /// Name of this module.
    pub fn name(&self) -> &str {
        &self.definition.name
    }

    /// Mount the module asynchronously (triggers async `on_created` if set,
    /// otherwise falls back to the sync handler).
    #[cfg(feature = "async")]
    pub async fn mount_async(&self) {
        {
            let mut mounted = self.mounted.lock().unwrap();
            if *mounted {
                return;
            }
            *mounted = true;
        }

        if let Some(ref handler) = self.definition.on_created_async {
            let current_state = self.state.lock().unwrap().get().clone();
            let ctx = self.global_context.clone();
            let new_state = handler(current_state, ctx).await;
            *self.state.lock().unwrap().get_mut() = new_state;
        } else if let Some(ref handler) = self.definition.on_created {
            let state = self.state.lock().unwrap();
            let ctx = self.global_context.as_deref();
            handler(state.get(), ctx);
        }
    }

    /// Unmount the module asynchronously (triggers async `on_destroyed` if set,
    /// otherwise falls back to the sync handler).
    #[cfg(feature = "async")]
    pub async fn unmount_async(&self) {
        {
            let mounted = self.mounted.lock().unwrap();
            if !*mounted {
                return;
            }
        }

        if let Some(ref handler) = self.definition.on_destroyed_async {
            let current_state = self.state.lock().unwrap().get().clone();
            let ctx = self.global_context.clone();
            let new_state = handler(current_state, ctx).await;
            *self.state.lock().unwrap().get_mut() = new_state;
        } else if let Some(ref handler) = self.definition.on_destroyed {
            let state = self.state.lock().unwrap();
            let ctx = self.global_context.as_deref();
            handler(state.get(), ctx);
        }

        *self.mounted.lock().unwrap() = false;
    }

    /// Dispatch an action asynchronously.
    ///
    /// Checks async handlers first, then falls back to sync handlers.
    #[cfg(feature = "async")]
    pub async fn dispatch_action_async(
        &self,
        name: impl Into<String>,
        payload: Option<Value>,
    ) -> Result<()> {
        let name = name.into();

        // Snapshot state before handler runs
        {
            let mut state = self.state.lock().unwrap();
            state.take_snapshot()?;
        }

        // Check async handlers first
        if let Some(handler) = self.definition.async_action_handlers.get(&name) {
            let current_state = self.state.lock().unwrap().get().clone();
            let ctx = self.global_context.clone();
            let new_state = handler(current_state, payload, ctx).await;
            *self.state.lock().unwrap().get_mut() = new_state;
            self.sync_state_to_engine()?;
            return Ok(());
        }

        // Fall back to sync handler
        let ctx = self.global_context.as_deref();
        if let Some(handler) = self.definition.action_handlers.get(&name) {
            let mut state = self.state.lock().unwrap();
            handler(state.get_mut(), payload.as_ref(), ctx);
        } else {
            return Err(SdkError::Engine(hypen_engine::EngineError::ActionNotFound(
                name,
            )));
        }

        self.sync_state_to_engine()?;
        Ok(())
    }

    /// Internal: compare state against pre-handler snapshot and push changes
    /// to the engine.
    fn sync_state_to_engine(&self) -> Result<()> {
        let state = self.state.lock().unwrap();
        let paths = state.changed_paths()?;

        if !paths.is_empty() {
            let patch = state.diff_patch()?;
            drop(state); // release lock before engine lock

            let mut engine = self.engine.lock().unwrap();
            engine.update_state(patch);
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::{Deserialize, Serialize};
    use std::sync::atomic::{AtomicI32, Ordering};

    #[derive(Clone, Default, Serialize, Deserialize, Debug)]
    struct TestState {
        count: i32,
        name: String,
    }

    #[test]
    fn test_module_builder_action() {
        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState {
                count: 0,
                name: "Alice".into(),
            })
            .on_action::<()>("increment", |state, _, _ctx| {
                state.count += 1;
            })
            .build();

        assert_eq!(def.name(), "Test");
        assert!(def.action_names().contains(&"increment".to_string()));
    }

    #[test]
    fn test_module_builder_with_ui() {
        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState::default())
            .ui(r#"Column { Text("Hello") }"#)
            .build();

        assert_eq!(def.ui_source(), Some(r#"Column { Text("Hello") }"#));
    }

    #[test]
    fn test_module_instance_dispatch() {
        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState {
                count: 0,
                name: "Alice".into(),
            })
            .on_action::<()>("increment", |state, _, _ctx| {
                state.count += 1;
            })
            .on_action::<String>("set_name", |state, name, _ctx| {
                state.name = name;
            })
            .build();

        let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
        instance.mount();

        instance.dispatch_action("increment", None).unwrap();
        assert_eq!(instance.get_state().count, 1);

        instance.dispatch_action("increment", None).unwrap();
        assert_eq!(instance.get_state().count, 2);

        instance
            .dispatch_action("set_name", Some(serde_json::json!("Bob")))
            .unwrap();
        assert_eq!(instance.get_state().name, "Bob");
    }

    #[test]
    fn test_module_lifecycle() {
        let created = Arc::new(AtomicI32::new(0));
        let destroyed = Arc::new(AtomicI32::new(0));

        let created_clone = created.clone();
        let destroyed_clone = destroyed.clone();

        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState::default())
            .on_created(move |_state, _ctx| {
                created_clone.fetch_add(1, Ordering::SeqCst);
            })
            .on_destroyed(move |_state, _ctx| {
                destroyed_clone.fetch_add(1, Ordering::SeqCst);
            })
            .build();

        let instance = ModuleInstance::new(Arc::new(def), None).unwrap();

        assert_eq!(created.load(Ordering::SeqCst), 0);
        instance.mount();
        assert_eq!(created.load(Ordering::SeqCst), 1);

        // Mounting again should be idempotent
        instance.mount();
        assert_eq!(created.load(Ordering::SeqCst), 1);

        instance.unmount();
        assert_eq!(destroyed.load(Ordering::SeqCst), 1);

        // Unmounting again should be idempotent
        instance.unmount();
        assert_eq!(destroyed.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn test_module_unknown_action() {
        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState::default())
            .build();

        let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
        let result = instance.dispatch_action("nonexistent", None);
        assert!(result.is_err());
    }

    #[test]
    fn test_module_persist_flag() {
        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState::default())
            .persist()
            .build();

        assert!(def.is_persistent());
    }

    #[test]
    fn test_module_typed_payload() {
        #[derive(Deserialize)]
        struct AddPayload {
            amount: i32,
        }

        let def = ModuleBuilder::<TestState>::new("TypedTest")
            .state(TestState {
                count: 10,
                name: "test".into(),
            })
            .on_action::<AddPayload>("add", |state, payload, _ctx| {
                state.count += payload.amount;
            })
            .on_action::<()>("reset", |state, _, _ctx| {
                state.count = 0;
            })
            .build();

        let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
        instance.mount();

        instance
            .dispatch_action("add", Some(serde_json::json!({"amount": 5})))
            .unwrap();
        assert_eq!(instance.get_state().count, 15);

        instance.dispatch_action("reset", None).unwrap();
        assert_eq!(instance.get_state().count, 0);
    }

    #[test]
    fn test_module_multiple_typed_actions() {
        #[derive(Deserialize)]
        struct AddPayload {
            amount: i32,
        }

        #[derive(Deserialize)]
        struct MultiplyPayload {
            factor: i32,
        }

        let def = ModuleBuilder::<TestState>::new("Mixed")
            .state(TestState {
                count: 10,
                name: "test".into(),
            })
            .on_action::<()>("reset", |state, _, _ctx| {
                state.count = 0;
            })
            .on_action::<AddPayload>("add", |state, payload, _ctx| {
                state.count += payload.amount;
            })
            .on_action::<MultiplyPayload>("multiply", |state, payload, _ctx| {
                state.count *= payload.factor;
            })
            .build();

        let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
        instance.mount();

        instance.dispatch_action("reset", None).unwrap();
        assert_eq!(instance.get_state().count, 0);

        instance
            .dispatch_action("add", Some(serde_json::json!({"amount": 5})))
            .unwrap();
        assert_eq!(instance.get_state().count, 5);

        instance
            .dispatch_action("multiply", Some(serde_json::json!({"factor": 3})))
            .unwrap();
        assert_eq!(instance.get_state().count, 15);
    }

    #[test]
    #[should_panic(expected = "ModuleBuilder::state() must be called before build()")]
    fn test_module_builder_panics_without_state() {
        let _def = ModuleBuilder::<TestState>::new("Test").build();
    }

    #[test]
    fn test_module_invalid_ui_source() {
        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState::default())
            .ui("this is not valid {{{{ hypen")
            .build();

        let result = ModuleInstance::new(Arc::new(def), None);
        assert!(result.is_err());
    }

    #[test]
    fn test_module_payload_type_mismatch_is_noop() {
        #[derive(Deserialize)]
        struct Expected {
            #[allow(dead_code)]
            value: i32,
        }

        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState {
                count: 42,
                name: "test".into(),
            })
            .on_action::<Expected>("set", |state, payload, _ctx| {
                state.count = payload.value;
            })
            .build();

        let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
        instance.mount();

        // Send wrong payload shape — handler should silently skip
        instance
            .dispatch_action("set", Some(serde_json::json!("wrong type")))
            .unwrap();
        assert_eq!(instance.get_state().count, 42); // unchanged
    }

    #[test]
    fn test_module_duplicate_action_last_wins() {
        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState {
                count: 0,
                name: "test".into(),
            })
            .on_action::<()>("act", |state, _, _ctx| {
                state.count += 1;
            })
            .on_action::<()>("act", |state, _, _ctx| {
                state.count += 100;
            })
            .build();

        let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
        instance.dispatch_action("act", None).unwrap();
        assert_eq!(instance.get_state().count, 100); // second handler wins
    }

    #[test]
    fn test_module_ui_file() {
        let dir = std::env::temp_dir().join("hypen_test_ui_file");
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).unwrap();

        let path = dir.join("counter.hypen");
        std::fs::write(&path, r#"Column { Text("Hello") }"#).unwrap();

        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState::default())
            .ui_file(path.to_str().unwrap())
            .build();

        let instance = ModuleInstance::new(Arc::new(def), None);
        assert!(instance.is_ok());

        let _ = std::fs::remove_dir_all(&dir);
    }

    #[test]
    fn test_module_ui_file_not_found() {
        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState::default())
            .ui_file("/tmp/hypen_no_such_file.hypen")
            .build();

        let result = ModuleInstance::new(Arc::new(def), None);
        assert!(result.is_err());
    }

    #[test]
    fn test_module_dispatch_without_mount() {
        let def = ModuleBuilder::<TestState>::new("Test")
            .state(TestState {
                count: 0,
                name: "test".into(),
            })
            .on_action::<()>("inc", |state, _, _ctx| {
                state.count += 1;
            })
            .build();

        let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
        // Don't mount — dispatch should still work
        instance.dispatch_action("inc", None).unwrap();
        assert_eq!(instance.get_state().count, 1);
    }

    #[test]
    fn test_module_raw_json_action() {
        let def = ModuleBuilder::<TestState>::new("RawTest")
            .state(TestState {
                count: 0,
                name: "test".into(),
            })
            .on_action::<Value>("set_count", |state, payload, _ctx| {
                if let Some(n) = payload.as_i64() {
                    state.count = n as i32;
                }
            })
            .build();

        let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
        instance.mount();

        instance
            .dispatch_action("set_count", Some(serde_json::json!(42)))
            .unwrap();
        assert_eq!(instance.get_state().count, 42);
    }

    // -----------------------------------------------------------------------
    // Async handler tests (behind "async" feature)
    // -----------------------------------------------------------------------

    #[cfg(feature = "async")]
    mod async_tests {
        use super::*;

        #[derive(Clone, Default, Serialize, Deserialize, Debug)]
        struct AsyncState {
            count: i32,
            name: String,
        }

        #[tokio::test]
        async fn test_async_action_handler() {
            let def = ModuleBuilder::<AsyncState>::new("AsyncTest")
                .state(AsyncState {
                    count: 0,
                    name: "test".into(),
                })
                .on_action_async::<()>("increment", |mut state, _, _ctx| {
                    Box::pin(async move {
                        state.count += 1;
                        state
                    })
                })
                .build();

            let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
            instance.mount();

            instance
                .dispatch_action_async("increment", None)
                .await
                .unwrap();
            assert_eq!(instance.get_state().count, 1);

            instance
                .dispatch_action_async("increment", None)
                .await
                .unwrap();
            assert_eq!(instance.get_state().count, 2);
        }

        #[tokio::test]
        async fn test_async_typed_payload() {
            #[derive(Deserialize)]
            struct AddPayload {
                amount: i32,
            }

            let def = ModuleBuilder::<AsyncState>::new("AsyncTyped")
                .state(AsyncState {
                    count: 10,
                    name: "test".into(),
                })
                .on_action_async::<AddPayload>("add", |mut state, payload, _ctx| {
                    Box::pin(async move {
                        state.count += payload.amount;
                        state
                    })
                })
                .build();

            let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
            instance.mount();

            instance
                .dispatch_action_async("add", Some(serde_json::json!({"amount": 5})))
                .await
                .unwrap();
            assert_eq!(instance.get_state().count, 15);
        }

        #[tokio::test]
        async fn test_async_falls_back_to_sync() {
            let def = ModuleBuilder::<AsyncState>::new("Fallback")
                .state(AsyncState {
                    count: 0,
                    name: "test".into(),
                })
                .on_action::<()>("sync_inc", |state, _, _ctx| {
                    state.count += 1;
                })
                .build();

            let instance = ModuleInstance::new(Arc::new(def), None).unwrap();

            // dispatch_action_async should fall back to the sync handler
            instance
                .dispatch_action_async("sync_inc", None)
                .await
                .unwrap();
            assert_eq!(instance.get_state().count, 1);
        }

        #[tokio::test]
        async fn test_async_on_created() {
            let def = ModuleBuilder::<AsyncState>::new("AsyncCreated")
                .state(AsyncState {
                    count: 0,
                    name: "test".into(),
                })
                .on_created_async(|mut state, _ctx| {
                    Box::pin(async move {
                        state.count = 42;
                        state.name = "initialized".into();
                        state
                    })
                })
                .build();

            let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
            instance.mount_async().await;

            assert_eq!(instance.get_state().count, 42);
            assert_eq!(instance.get_state().name, "initialized");
        }

        #[tokio::test]
        async fn test_async_on_destroyed() {
            let destroyed = Arc::new(std::sync::atomic::AtomicBool::new(false));
            let destroyed_clone = destroyed.clone();

            let def = ModuleBuilder::<AsyncState>::new("AsyncDestroyed")
                .state(AsyncState {
                    count: 0,
                    name: "test".into(),
                })
                .on_destroyed_async(move |state, _ctx| {
                    let flag = destroyed_clone.clone();
                    Box::pin(async move {
                        flag.store(true, std::sync::atomic::Ordering::SeqCst);
                        state
                    })
                })
                .build();

            let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
            instance.mount();
            assert!(!destroyed.load(std::sync::atomic::Ordering::SeqCst));

            instance.unmount_async().await;
            assert!(destroyed.load(std::sync::atomic::Ordering::SeqCst));
            assert!(!instance.is_mounted());
        }

        #[tokio::test]
        async fn test_async_mount_idempotent() {
            let call_count = Arc::new(std::sync::atomic::AtomicI32::new(0));
            let cc = call_count.clone();

            let def = ModuleBuilder::<AsyncState>::new("Idempotent")
                .state(AsyncState::default())
                .on_created_async(move |state, _ctx| {
                    let count = cc.clone();
                    Box::pin(async move {
                        count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
                        state
                    })
                })
                .build();

            let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
            instance.mount_async().await;
            instance.mount_async().await; // second call is noop

            assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
        }

        #[tokio::test]
        async fn test_async_dispatch_unknown_action() {
            let def = ModuleBuilder::<AsyncState>::new("Unknown")
                .state(AsyncState::default())
                .build();

            let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
            let result = instance.dispatch_action_async("nonexistent", None).await;
            assert!(result.is_err());
        }

        #[tokio::test]
        async fn test_async_mixed_sync_and_async_actions() {
            #[derive(Deserialize)]
            struct SetName {
                name: String,
            }

            let def = ModuleBuilder::<AsyncState>::new("Mixed")
                .state(AsyncState {
                    count: 0,
                    name: "init".into(),
                })
                .on_action::<()>("sync_inc", |state, _, _ctx| {
                    state.count += 1;
                })
                .on_action_async::<SetName>("async_set_name", |mut state, payload, _ctx| {
                    Box::pin(async move {
                        state.name = payload.name;
                        state
                    })
                })
                .build();

            let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
            instance.mount();

            // Use async dispatch for both sync and async handlers
            instance
                .dispatch_action_async("sync_inc", None)
                .await
                .unwrap();
            assert_eq!(instance.get_state().count, 1);

            instance
                .dispatch_action_async("async_set_name", Some(serde_json::json!({"name": "Alice"})))
                .await
                .unwrap();
            assert_eq!(instance.get_state().name, "Alice");
        }
    }
}
