mockforge_http/
state_machine_api.rs

1//! State machine API handlers
2//!
3//! Provides REST endpoints for managing scenario state machines, including
4//! CRUD operations, execution, and import/export functionality.
5
6use axum::{
7    extract::{Path, State},
8    http::StatusCode,
9    Json,
10};
11use mockforge_core::intelligent_behavior::{rules::StateMachine, visual_layout::VisualLayout};
12use mockforge_scenarios::ScenarioManifest;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::HashMap;
16use tracing::error;
17
18// Re-export ManagementState for use in handlers
19use crate::management::ManagementState;
20
21// ===== Request/Response Types =====
22
23/// Request to create or update a state machine
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct StateMachineRequest {
26    /// State machine definition
27    pub state_machine: StateMachine,
28    /// Optional visual layout
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub visual_layout: Option<VisualLayout>,
31}
32
33/// Request to execute a state transition
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct TransitionRequest {
36    /// Resource ID to transition
37    pub resource_id: String,
38    /// Target state
39    pub to_state: String,
40    /// Optional context variables for condition evaluation
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub context: Option<HashMap<String, Value>>,
43}
44
45/// Request to create a state instance
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct CreateInstanceRequest {
48    /// Resource ID
49    pub resource_id: String,
50    /// Resource type (must match a state machine resource_type)
51    pub resource_type: String,
52}
53
54/// Response for state machine operations
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct StateMachineResponse {
57    /// State machine definition
58    pub state_machine: StateMachine,
59    /// Optional visual layout
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub visual_layout: Option<VisualLayout>,
62}
63
64/// Response for listing state machines
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct StateMachineListResponse {
67    /// List of state machines
68    pub state_machines: Vec<StateMachineInfo>,
69    /// Total count
70    pub total: usize,
71}
72
73/// Information about a state machine
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct StateMachineInfo {
76    /// Resource type
77    pub resource_type: String,
78    /// Number of states
79    pub state_count: usize,
80    /// Number of transitions
81    pub transition_count: usize,
82    /// Number of sub-scenarios
83    pub sub_scenario_count: usize,
84    /// Whether it has a visual layout
85    pub has_visual_layout: bool,
86}
87
88/// Response for state instance operations
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct StateInstanceResponse {
91    /// Resource ID
92    pub resource_id: String,
93    /// Current state
94    pub current_state: String,
95    /// Resource type
96    pub resource_type: String,
97    /// State history count
98    pub history_count: usize,
99    /// State data
100    pub state_data: HashMap<String, Value>,
101}
102
103/// Response for listing state instances
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct StateInstanceListResponse {
106    /// List of instances
107    pub instances: Vec<StateInstanceResponse>,
108    /// Total count
109    pub total: usize,
110}
111
112/// Response for next states query
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct NextStatesResponse {
115    /// List of possible next states
116    pub next_states: Vec<String>,
117}
118
119/// Response for import/export operations
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct ImportExportResponse {
122    /// State machines
123    pub state_machines: Vec<StateMachine>,
124    /// Visual layouts by resource type
125    pub visual_layouts: HashMap<String, VisualLayout>,
126}
127
128// ===== Handlers =====
129
130/// List all state machines
131pub async fn list_state_machines(
132    State(state): State<ManagementState>,
133) -> Result<Json<StateMachineListResponse>, StatusCode> {
134    let manager = state.state_machine_manager.read().await;
135
136    // Get all state machines
137    let machines = manager.list_state_machines().await;
138
139    // Check visual layouts separately for each state machine
140    // We need to check if a visual layout exists for each state machine
141    let mut state_machine_list = Vec::new();
142    for (resource_type, sm) in machines.iter() {
143        let has_visual_layout = manager.get_visual_layout(resource_type).await.is_some();
144        state_machine_list.push(StateMachineInfo {
145            resource_type: resource_type.clone(),
146            state_count: sm.states.len(),
147            transition_count: sm.transitions.len(),
148            sub_scenario_count: sm.sub_scenarios.len(),
149            has_visual_layout,
150        });
151    }
152
153    Ok(Json(StateMachineListResponse {
154        state_machines: state_machine_list.clone(),
155        total: state_machine_list.len(),
156    }))
157}
158
159/// Get a state machine by resource type
160pub async fn get_state_machine(
161    State(state): State<ManagementState>,
162    Path(resource_type): Path<String>,
163) -> Result<Json<StateMachineResponse>, StatusCode> {
164    let manager = state.state_machine_manager.read().await;
165
166    let state_machine =
167        manager.get_state_machine(&resource_type).await.ok_or(StatusCode::NOT_FOUND)?;
168
169    let visual_layout = manager.get_visual_layout(&resource_type).await;
170
171    // Convert types from mockforge-scenarios' dependency version to local version
172    // by serializing and deserializing through JSON
173    let state_machine_json =
174        serde_json::to_value(&state_machine).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
175    let state_machine: StateMachine = serde_json::from_value(state_machine_json)
176        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
177
178    let visual_layout: Option<VisualLayout> = visual_layout
179        .map(|layout| {
180            let layout_json =
181                serde_json::to_value(&layout).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
182            serde_json::from_value(layout_json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
183        })
184        .transpose()
185        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
186
187    Ok(Json(StateMachineResponse {
188        state_machine,
189        visual_layout,
190    }))
191}
192
193/// Create or update a state machine
194pub async fn create_state_machine(
195    State(state): State<ManagementState>,
196    Json(request): Json<StateMachineRequest>,
197) -> Result<Json<StateMachineResponse>, StatusCode> {
198    let manager = state.state_machine_manager.write().await;
199
200    // Convert types from local version to mockforge-scenarios' dependency version
201    // by serializing and deserializing through JSON
202    // The ScenarioManifest uses types from mockforge-scenarios' mockforge-core dependency (0.2.9)
203    // We need to convert our local StateMachine to that version
204    let state_machine_json = serde_json::to_value(&request.state_machine)
205        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
206
207    // Create manifest with JSON values - serde will deserialize into the correct types
208    // We need to provide all required fields for ScenarioManifest
209    let mut manifest_json = serde_json::json!({
210        "manifest_version": "1.0",
211        "name": "api",
212        "version": "1.0.0",
213        "title": "API State Machine",
214        "description": "State machine created via API",
215        "author": "api",
216        "category": "other",
217        "compatibility": {
218            "min_version": "0.1.0",
219            "max_version": null
220        },
221        "files": [],
222        "state_machines": [state_machine_json],
223        "state_machine_graphs": {}
224    });
225
226    if let Some(layout) = &request.visual_layout {
227        let layout_json =
228            serde_json::to_value(layout).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
229        manifest_json["state_machine_graphs"][&request.state_machine.resource_type] = layout_json;
230    }
231
232    let manifest: ScenarioManifest =
233        serde_json::from_value(manifest_json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
234
235    // Validate the first state machine from manifest
236    if let Some(sm) = manifest.state_machines.first() {
237        if let Err(e) = manager.validate_state_machine(sm) {
238            error!("Invalid state machine: {}", e);
239            return Err(StatusCode::BAD_REQUEST);
240        }
241    }
242
243    if let Err(e) = manager.load_from_manifest(&manifest).await {
244        error!("Failed to load state machine: {}", e);
245        return Err(StatusCode::INTERNAL_SERVER_ERROR);
246    }
247
248    // Visual layout is already set in the manifest, no need to set separately
249
250    // Broadcast WebSocket event
251    if let Some(ref ws_tx) = state.ws_broadcast {
252        let event = crate::management_ws::MockEvent::state_machine_updated(
253            request.state_machine.resource_type.clone(),
254            request.state_machine.clone(),
255        );
256        let _ = ws_tx.send(event);
257    }
258
259    // Get state machine and layout back after loading (returns version from mockforge-scenarios' dependency)
260    let state_machine_from_manager = manager
261        .get_state_machine(&request.state_machine.resource_type)
262        .await
263        .ok_or(StatusCode::NOT_FOUND)?;
264    let visual_layout_from_manager =
265        manager.get_visual_layout(&request.state_machine.resource_type).await;
266
267    // Convert back to local types
268    let state_machine_json = serde_json::to_value(&state_machine_from_manager)
269        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
270    let state_machine: StateMachine = serde_json::from_value(state_machine_json)
271        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
272
273    let visual_layout: Option<VisualLayout> = visual_layout_from_manager
274        .map(|layout| {
275            let layout_json =
276                serde_json::to_value(&layout).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
277            serde_json::from_value(layout_json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
278        })
279        .transpose()
280        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
281
282    Ok(Json(StateMachineResponse {
283        state_machine,
284        visual_layout,
285    }))
286}
287
288/// Delete a state machine
289pub async fn delete_state_machine(
290    State(state): State<ManagementState>,
291    Path(resource_type): Path<String>,
292) -> Result<StatusCode, StatusCode> {
293    let manager = state.state_machine_manager.write().await;
294
295    // Delete the state machine
296    let deleted = manager.delete_state_machine(&resource_type).await;
297
298    if !deleted {
299        return Err(StatusCode::NOT_FOUND);
300    }
301
302    // Broadcast WebSocket event
303    if let Some(ref ws_tx) = state.ws_broadcast {
304        let event = crate::management_ws::MockEvent::state_machine_deleted(resource_type);
305        let _ = ws_tx.send(event);
306    }
307
308    Ok(StatusCode::NO_CONTENT)
309}
310
311/// List all state instances
312pub async fn list_instances(
313    State(state): State<ManagementState>,
314) -> Result<Json<StateInstanceListResponse>, StatusCode> {
315    let manager = state.state_machine_manager.read().await;
316
317    let instances = manager.list_instances().await;
318
319    let instance_responses: Vec<StateInstanceResponse> = instances
320        .iter()
321        .map(|i| StateInstanceResponse {
322            resource_id: i.resource_id.clone(),
323            current_state: i.current_state.clone(),
324            resource_type: i.resource_type.clone(),
325            history_count: i.state_history.len(),
326            state_data: i.state_data.clone(),
327        })
328        .collect();
329
330    Ok(Json(StateInstanceListResponse {
331        instances: instance_responses,
332        total: instances.len(),
333    }))
334}
335
336/// Get a state instance by resource ID
337pub async fn get_instance(
338    State(state): State<ManagementState>,
339    Path(resource_id): Path<String>,
340) -> Result<Json<StateInstanceResponse>, StatusCode> {
341    let manager = state.state_machine_manager.read().await;
342
343    let instance = manager.get_instance(&resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
344
345    Ok(Json(StateInstanceResponse {
346        resource_id: instance.resource_id,
347        current_state: instance.current_state,
348        resource_type: instance.resource_type,
349        history_count: instance.state_history.len(),
350        state_data: instance.state_data,
351    }))
352}
353
354/// Create a new state instance
355pub async fn create_instance(
356    State(state): State<ManagementState>,
357    Json(request): Json<CreateInstanceRequest>,
358) -> Result<Json<StateInstanceResponse>, StatusCode> {
359    let manager = state.state_machine_manager.write().await;
360
361    if let Err(e) = manager.create_instance(&request.resource_id, &request.resource_type).await {
362        error!("Failed to create instance: {}", e);
363        return Err(StatusCode::BAD_REQUEST);
364    }
365
366    let instance = manager
367        .get_instance(&request.resource_id)
368        .await
369        .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
370
371    // Broadcast WebSocket event
372    if let Some(ref ws_tx) = state.ws_broadcast {
373        let event = crate::management_ws::MockEvent::state_instance_created(
374            instance.resource_id.clone(),
375            instance.resource_type.clone(),
376            instance.current_state.clone(),
377        );
378        let _ = ws_tx.send(event);
379    }
380
381    Ok(Json(StateInstanceResponse {
382        resource_id: instance.resource_id,
383        current_state: instance.current_state,
384        resource_type: instance.resource_type,
385        history_count: instance.state_history.len(),
386        state_data: instance.state_data,
387    }))
388}
389
390/// Execute a state transition
391pub async fn execute_transition(
392    State(state): State<ManagementState>,
393    Json(request): Json<TransitionRequest>,
394) -> Result<Json<StateInstanceResponse>, StatusCode> {
395    let manager = state.state_machine_manager.write().await;
396
397    if let Err(e) = manager
398        .execute_transition(&request.resource_id, &request.to_state, request.context)
399        .await
400    {
401        error!("Failed to execute transition: {}", e);
402        return Err(StatusCode::BAD_REQUEST);
403    }
404
405    let instance = manager.get_instance(&request.resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
406
407    // Get the previous state from history if available
408    let from_state = instance
409        .state_history
410        .last()
411        .map(|h| h.from_state.clone())
412        .unwrap_or_else(|| instance.current_state.clone());
413
414    // Broadcast WebSocket event
415    if let Some(ref ws_tx) = state.ws_broadcast {
416        let event = crate::management_ws::MockEvent::state_transitioned(
417            instance.resource_id.clone(),
418            instance.resource_type.clone(),
419            from_state,
420            instance.current_state.clone(),
421            instance.state_data.clone(),
422        );
423        let _ = ws_tx.send(event);
424    }
425
426    Ok(Json(StateInstanceResponse {
427        resource_id: instance.resource_id,
428        current_state: instance.current_state,
429        resource_type: instance.resource_type,
430        history_count: instance.state_history.len(),
431        state_data: instance.state_data,
432    }))
433}
434
435/// Get next possible states for a resource
436pub async fn get_next_states(
437    State(state): State<ManagementState>,
438    Path(resource_id): Path<String>,
439) -> Result<Json<NextStatesResponse>, StatusCode> {
440    let manager = state.state_machine_manager.read().await;
441
442    let next_states =
443        manager.get_next_states(&resource_id).await.map_err(|_| StatusCode::NOT_FOUND)?;
444
445    Ok(Json(NextStatesResponse { next_states }))
446}
447
448/// Get current state of a resource
449pub async fn get_current_state(
450    State(state): State<ManagementState>,
451    Path(resource_id): Path<String>,
452) -> Result<Json<serde_json::Value>, StatusCode> {
453    let manager = state.state_machine_manager.read().await;
454
455    let current_state =
456        manager.get_current_state(&resource_id).await.ok_or(StatusCode::NOT_FOUND)?;
457
458    Ok(Json(serde_json::json!({
459        "resource_id": resource_id,
460        "current_state": current_state
461    })))
462}
463
464/// Export state machines as JSON
465pub async fn export_state_machines(
466    State(state): State<ManagementState>,
467) -> Result<Json<ImportExportResponse>, StatusCode> {
468    let manager = state.state_machine_manager.read().await;
469
470    // Export all state machines and visual layouts (returns versions from mockforge-scenarios' dependency)
471    let (state_machines_from_manager, visual_layouts_from_manager) = manager.export_all().await;
472
473    // Convert to local types by serializing and deserializing
474    let state_machines: Vec<StateMachine> = state_machines_from_manager
475        .into_iter()
476        .map(|sm| {
477            let json = serde_json::to_value(&sm).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
478            serde_json::from_value(json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
479        })
480        .collect::<Result<Vec<_>, StatusCode>>()?;
481
482    let visual_layouts: HashMap<String, VisualLayout> = visual_layouts_from_manager
483        .into_iter()
484        .map(|(k, v)| {
485            let json = serde_json::to_value(&v).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
486            let layout: VisualLayout =
487                serde_json::from_value(json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
488            Ok((k, layout))
489        })
490        .collect::<Result<HashMap<_, _>, StatusCode>>()?;
491
492    Ok(Json(ImportExportResponse {
493        state_machines,
494        visual_layouts,
495    }))
496}
497
498/// Import state machines from JSON
499pub async fn import_state_machines(
500    State(state): State<ManagementState>,
501    Json(request): Json<ImportExportResponse>,
502) -> Result<StatusCode, StatusCode> {
503    let manager = state.state_machine_manager.write().await;
504
505    // Create manifest from JSON to let serde handle type conversion
506    // We need to provide all required fields for ScenarioManifest
507    let manifest_json = serde_json::json!({
508        "manifest_version": "1.0",
509        "name": "imported",
510        "version": "1.0.0",
511        "title": "Imported State Machines",
512        "description": "State machines imported via API",
513        "author": "api",
514        "category": "other",
515        "compatibility": {
516            "min_version": "0.1.0",
517            "max_version": null
518        },
519        "files": [],
520        "state_machines": request.state_machines,
521        "state_machine_graphs": request.visual_layouts
522    });
523
524    let manifest: ScenarioManifest =
525        serde_json::from_value(manifest_json).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
526
527    if let Err(e) = manager.load_from_manifest(&manifest).await {
528        error!("Failed to import state machines: {}", e);
529        return Err(StatusCode::BAD_REQUEST);
530    }
531
532    // Visual layouts are already set in the manifest, no need to set separately
533
534    Ok(StatusCode::CREATED)
535}
536
537/// Create the state machine API router
538///
539/// This function creates routes that use ManagementState, so they can be
540/// nested within the management router.
541pub fn create_state_machine_routes() -> axum::Router<ManagementState> {
542    use axum::{
543        routing::{delete, get, post, put},
544        Router,
545    };
546
547    Router::new()
548        // State machine CRUD
549        .route("/", get(list_state_machines))
550        .route("/", post(create_state_machine))
551        .route("/{resource_type}", get(get_state_machine))
552        .route("/{resource_type}", put(create_state_machine))
553        .route("/{resource_type}", delete(delete_state_machine))
554
555        // State instance operations
556        .route("/instances", get(list_instances))
557        .route("/instances", post(create_instance))
558        .route("/instances/{resource_id}", get(get_instance))
559        .route("/instances/{resource_id}/state", get(get_current_state))
560        .route("/instances/{resource_id}/next-states", get(get_next_states))
561        .route("/instances/{resource_id}/transition", post(execute_transition))
562
563        // Import/Export
564        .route("/export", get(export_state_machines))
565        .route("/import", post(import_state_machines))
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571    use std::collections::HashMap;
572
573    fn create_test_state_machine() -> StateMachine {
574        StateMachine::new(
575            "test-resource",
576            vec!["pending".to_string(), "active".to_string()],
577            "pending",
578        )
579    }
580
581    // StateMachineRequest tests
582    #[test]
583    fn test_state_machine_request_serialize() {
584        let request = StateMachineRequest {
585            state_machine: create_test_state_machine(),
586            visual_layout: None,
587        };
588        let json = serde_json::to_string(&request);
589        assert!(json.is_ok());
590    }
591
592    #[test]
593    fn test_state_machine_request_deserialize() {
594        let json = r#"{"state_machine":{"resource_type":"test","states":["a","b"],"initial_state":"a","transitions":[],"sub_scenarios":[]}}"#;
595        let result: Result<StateMachineRequest, _> = serde_json::from_str(json);
596        assert!(result.is_ok());
597        assert!(result.unwrap().visual_layout.is_none());
598    }
599
600    #[test]
601    fn test_state_machine_request_clone() {
602        let request = StateMachineRequest {
603            state_machine: create_test_state_machine(),
604            visual_layout: None,
605        };
606        let cloned = request.clone();
607        assert!(cloned.visual_layout.is_none());
608    }
609
610    #[test]
611    fn test_state_machine_request_debug() {
612        let request = StateMachineRequest {
613            state_machine: create_test_state_machine(),
614            visual_layout: None,
615        };
616        let debug = format!("{:?}", request);
617        assert!(debug.contains("StateMachineRequest"));
618    }
619
620    // TransitionRequest tests
621    #[test]
622    fn test_transition_request_new() {
623        let request = TransitionRequest {
624            resource_id: "order-123".to_string(),
625            to_state: "shipped".to_string(),
626            context: None,
627        };
628        assert_eq!(request.resource_id, "order-123");
629        assert_eq!(request.to_state, "shipped");
630        assert!(request.context.is_none());
631    }
632
633    #[test]
634    fn test_transition_request_with_context() {
635        let mut context = HashMap::new();
636        context.insert("priority".to_string(), serde_json::json!("high"));
637
638        let request = TransitionRequest {
639            resource_id: "order-123".to_string(),
640            to_state: "shipped".to_string(),
641            context: Some(context),
642        };
643        assert!(request.context.is_some());
644        assert_eq!(request.context.unwrap().get("priority"), Some(&serde_json::json!("high")));
645    }
646
647    #[test]
648    fn test_transition_request_serialize() {
649        let request = TransitionRequest {
650            resource_id: "test".to_string(),
651            to_state: "active".to_string(),
652            context: None,
653        };
654        let json = serde_json::to_string(&request).unwrap();
655        assert!(json.contains("resource_id"));
656        assert!(json.contains("to_state"));
657        assert!(!json.contains("context")); // skip_serializing_if removes None
658    }
659
660    #[test]
661    fn test_transition_request_deserialize() {
662        let json = r#"{"resource_id":"test","to_state":"active"}"#;
663        let request: TransitionRequest = serde_json::from_str(json).unwrap();
664        assert_eq!(request.resource_id, "test");
665        assert_eq!(request.to_state, "active");
666    }
667
668    // CreateInstanceRequest tests
669    #[test]
670    fn test_create_instance_request_new() {
671        let request = CreateInstanceRequest {
672            resource_id: "order-456".to_string(),
673            resource_type: "order".to_string(),
674        };
675        assert_eq!(request.resource_id, "order-456");
676        assert_eq!(request.resource_type, "order");
677    }
678
679    #[test]
680    fn test_create_instance_request_serialize() {
681        let request = CreateInstanceRequest {
682            resource_id: "test-id".to_string(),
683            resource_type: "test-type".to_string(),
684        };
685        let json = serde_json::to_string(&request).unwrap();
686        assert!(json.contains("test-id"));
687        assert!(json.contains("test-type"));
688    }
689
690    #[test]
691    fn test_create_instance_request_deserialize() {
692        let json = r#"{"resource_id":"id-1","resource_type":"type-1"}"#;
693        let request: CreateInstanceRequest = serde_json::from_str(json).unwrap();
694        assert_eq!(request.resource_id, "id-1");
695        assert_eq!(request.resource_type, "type-1");
696    }
697
698    // StateMachineResponse tests
699    #[test]
700    fn test_state_machine_response_without_layout() {
701        let response = StateMachineResponse {
702            state_machine: create_test_state_machine(),
703            visual_layout: None,
704        };
705        let json = serde_json::to_string(&response).unwrap();
706        assert!(!json.contains("visual_layout")); // skip_serializing_if
707    }
708
709    #[test]
710    fn test_state_machine_response_clone() {
711        let response = StateMachineResponse {
712            state_machine: create_test_state_machine(),
713            visual_layout: None,
714        };
715        let cloned = response.clone();
716        assert!(cloned.visual_layout.is_none());
717    }
718
719    // StateMachineListResponse tests
720    #[test]
721    fn test_state_machine_list_response_empty() {
722        let response = StateMachineListResponse {
723            state_machines: vec![],
724            total: 0,
725        };
726        assert_eq!(response.total, 0);
727        assert!(response.state_machines.is_empty());
728    }
729
730    #[test]
731    fn test_state_machine_list_response_with_items() {
732        let info = StateMachineInfo {
733            resource_type: "order".to_string(),
734            state_count: 5,
735            transition_count: 10,
736            sub_scenario_count: 2,
737            has_visual_layout: true,
738        };
739        let response = StateMachineListResponse {
740            state_machines: vec![info],
741            total: 1,
742        };
743        assert_eq!(response.total, 1);
744        assert_eq!(response.state_machines[0].resource_type, "order");
745    }
746
747    #[test]
748    fn test_state_machine_list_response_serialize() {
749        let response = StateMachineListResponse {
750            state_machines: vec![],
751            total: 0,
752        };
753        let json = serde_json::to_string(&response).unwrap();
754        assert!(json.contains("state_machines"));
755        assert!(json.contains("total"));
756    }
757
758    // StateMachineInfo tests
759    #[test]
760    fn test_state_machine_info_new() {
761        let info = StateMachineInfo {
762            resource_type: "user".to_string(),
763            state_count: 3,
764            transition_count: 5,
765            sub_scenario_count: 1,
766            has_visual_layout: false,
767        };
768        assert_eq!(info.resource_type, "user");
769        assert_eq!(info.state_count, 3);
770        assert_eq!(info.transition_count, 5);
771        assert_eq!(info.sub_scenario_count, 1);
772        assert!(!info.has_visual_layout);
773    }
774
775    #[test]
776    fn test_state_machine_info_clone() {
777        let info = StateMachineInfo {
778            resource_type: "product".to_string(),
779            state_count: 4,
780            transition_count: 8,
781            sub_scenario_count: 0,
782            has_visual_layout: true,
783        };
784        let cloned = info.clone();
785        assert_eq!(info.resource_type, cloned.resource_type);
786        assert_eq!(info.state_count, cloned.state_count);
787    }
788
789    #[test]
790    fn test_state_machine_info_serialize() {
791        let info = StateMachineInfo {
792            resource_type: "item".to_string(),
793            state_count: 2,
794            transition_count: 3,
795            sub_scenario_count: 0,
796            has_visual_layout: false,
797        };
798        let json = serde_json::to_string(&info).unwrap();
799        assert!(json.contains("\"resource_type\":\"item\""));
800        assert!(json.contains("\"state_count\":2"));
801    }
802
803    // StateInstanceResponse tests
804    #[test]
805    fn test_state_instance_response_new() {
806        let response = StateInstanceResponse {
807            resource_id: "order-1".to_string(),
808            current_state: "pending".to_string(),
809            resource_type: "order".to_string(),
810            history_count: 0,
811            state_data: HashMap::new(),
812        };
813        assert_eq!(response.resource_id, "order-1");
814        assert_eq!(response.current_state, "pending");
815        assert_eq!(response.history_count, 0);
816    }
817
818    #[test]
819    fn test_state_instance_response_with_data() {
820        let mut state_data = HashMap::new();
821        state_data.insert("total".to_string(), serde_json::json!(100.50));
822
823        let response = StateInstanceResponse {
824            resource_id: "order-2".to_string(),
825            current_state: "confirmed".to_string(),
826            resource_type: "order".to_string(),
827            history_count: 3,
828            state_data,
829        };
830        assert_eq!(response.history_count, 3);
831        assert!(response.state_data.contains_key("total"));
832    }
833
834    #[test]
835    fn test_state_instance_response_serialize() {
836        let response = StateInstanceResponse {
837            resource_id: "test".to_string(),
838            current_state: "active".to_string(),
839            resource_type: "resource".to_string(),
840            history_count: 5,
841            state_data: HashMap::new(),
842        };
843        let json = serde_json::to_string(&response).unwrap();
844        assert!(json.contains("resource_id"));
845        assert!(json.contains("current_state"));
846        assert!(json.contains("history_count"));
847    }
848
849    // StateInstanceListResponse tests
850    #[test]
851    fn test_state_instance_list_response_empty() {
852        let response = StateInstanceListResponse {
853            instances: vec![],
854            total: 0,
855        };
856        assert_eq!(response.total, 0);
857        assert!(response.instances.is_empty());
858    }
859
860    #[test]
861    fn test_state_instance_list_response_with_instances() {
862        let instance = StateInstanceResponse {
863            resource_id: "inst-1".to_string(),
864            current_state: "ready".to_string(),
865            resource_type: "service".to_string(),
866            history_count: 2,
867            state_data: HashMap::new(),
868        };
869        let response = StateInstanceListResponse {
870            instances: vec![instance],
871            total: 1,
872        };
873        assert_eq!(response.total, 1);
874    }
875
876    // NextStatesResponse tests
877    #[test]
878    fn test_next_states_response_empty() {
879        let response = NextStatesResponse {
880            next_states: vec![],
881        };
882        assert!(response.next_states.is_empty());
883    }
884
885    #[test]
886    fn test_next_states_response_with_states() {
887        let response = NextStatesResponse {
888            next_states: vec!["shipped".to_string(), "cancelled".to_string()],
889        };
890        assert_eq!(response.next_states.len(), 2);
891        assert!(response.next_states.contains(&"shipped".to_string()));
892    }
893
894    #[test]
895    fn test_next_states_response_serialize() {
896        let response = NextStatesResponse {
897            next_states: vec!["state1".to_string(), "state2".to_string()],
898        };
899        let json = serde_json::to_string(&response).unwrap();
900        assert!(json.contains("state1"));
901        assert!(json.contains("state2"));
902    }
903
904    // ImportExportResponse tests
905    #[test]
906    fn test_import_export_response_empty() {
907        let response = ImportExportResponse {
908            state_machines: vec![],
909            visual_layouts: HashMap::new(),
910        };
911        assert!(response.state_machines.is_empty());
912        assert!(response.visual_layouts.is_empty());
913    }
914
915    #[test]
916    fn test_import_export_response_serialize() {
917        let response = ImportExportResponse {
918            state_machines: vec![],
919            visual_layouts: HashMap::new(),
920        };
921        let json = serde_json::to_string(&response).unwrap();
922        assert!(json.contains("state_machines"));
923        assert!(json.contains("visual_layouts"));
924    }
925
926    #[test]
927    fn test_import_export_response_deserialize() {
928        let json = r#"{"state_machines":[],"visual_layouts":{}}"#;
929        let response: ImportExportResponse = serde_json::from_str(json).unwrap();
930        assert!(response.state_machines.is_empty());
931    }
932
933    // Router tests
934    #[test]
935    fn test_create_state_machine_routes() {
936        let router = create_state_machine_routes();
937        // Just verify it creates without panicking
938        let _ = format!("{:?}", router);
939    }
940}