mockforge_core/
stateful_handler.rs

1//! Stateful response handler for HTTP requests
2//!
3//! Integrates state machines with HTTP request handling to provide dynamic responses
4//! based on request history and state transitions.
5
6use crate::{Error, Result};
7use axum::http::{HeaderMap, Method, Uri};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tracing::debug;
14
15/// Simple state instance for tracking resource state
16#[derive(Debug, Clone)]
17struct StateInstance {
18    /// Resource identifier
19    resource_id: String,
20    /// Current state
21    current_state: String,
22    /// Resource type
23    resource_type: String,
24    /// State data (key-value pairs)
25    state_data: HashMap<String, Value>,
26}
27
28impl StateInstance {
29    fn new(resource_id: String, resource_type: String, initial_state: String) -> Self {
30        Self {
31            resource_id,
32            current_state: initial_state,
33            resource_type,
34            state_data: HashMap::new(),
35        }
36    }
37
38    fn transition_to(&mut self, new_state: String) {
39        self.current_state = new_state;
40    }
41}
42
43/// Simple state machine manager for stateful responses
44struct StateMachineManager {
45    /// State instances by resource ID
46    instances: Arc<RwLock<HashMap<String, StateInstance>>>,
47}
48
49impl StateMachineManager {
50    fn new() -> Self {
51        Self {
52            instances: Arc::new(RwLock::new(HashMap::new())),
53        }
54    }
55
56    async fn get_or_create_instance(
57        &self,
58        resource_id: String,
59        resource_type: String,
60        initial_state: String,
61    ) -> Result<StateInstance> {
62        let mut instances = self.instances.write().await;
63        if let Some(instance) = instances.get(&resource_id) {
64            Ok(instance.clone())
65        } else {
66            let instance = StateInstance::new(resource_id.clone(), resource_type, initial_state);
67            instances.insert(resource_id, instance.clone());
68            Ok(instance)
69        }
70    }
71
72    async fn update_instance(&self, resource_id: String, instance: StateInstance) -> Result<()> {
73        let mut instances = self.instances.write().await;
74        instances.insert(resource_id, instance);
75        Ok(())
76    }
77}
78
79/// Configuration for stateful response handling
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct StatefulConfig {
82    /// Resource ID extraction configuration
83    pub resource_id_extract: ResourceIdExtract,
84    /// Resource type for this endpoint
85    pub resource_type: String,
86    /// State-based response configurations
87    pub state_responses: HashMap<String, StateResponse>,
88    /// Transition triggers (method + path combinations)
89    pub transitions: Vec<TransitionTrigger>,
90}
91
92/// Resource ID extraction configuration
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(tag = "type", rename_all = "snake_case")]
95pub enum ResourceIdExtract {
96    /// Extract from path parameter (e.g., "/orders/{order_id}" -> extract "order_id")
97    PathParam {
98        /// Path parameter name to extract
99        param: String,
100    },
101    /// Extract from JSONPath in request body
102    JsonPath {
103        /// JSONPath expression to extract the resource ID
104        path: String,
105    },
106    /// Extract from header value
107    Header {
108        /// Header name to extract the resource ID from
109        name: String,
110    },
111    /// Extract from query parameter
112    QueryParam {
113        /// Query parameter name to extract
114        param: String,
115    },
116    /// Use a combination of values
117    Composite {
118        /// List of extractors to try in order
119        extractors: Vec<ResourceIdExtract>,
120    },
121}
122
123/// State-based response configuration
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct StateResponse {
126    /// HTTP status code for this state
127    pub status_code: u16,
128    /// Response headers
129    pub headers: HashMap<String, String>,
130    /// Response body template
131    pub body_template: String,
132    /// Content type
133    pub content_type: String,
134}
135
136/// Transition trigger configuration
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct TransitionTrigger {
139    /// HTTP method that triggers this transition (as string)
140    #[serde(with = "method_serde")]
141    pub method: Method,
142    /// Path pattern that triggers this transition
143    pub path_pattern: String,
144    /// Source state
145    pub from_state: String,
146    /// Target state
147    pub to_state: String,
148    /// Optional condition (JSONPath expression)
149    pub condition: Option<String>,
150}
151
152mod method_serde {
153    use axum::http::Method;
154    use serde::{Deserialize, Deserializer, Serialize, Serializer};
155
156    pub fn serialize<S>(method: &Method, serializer: S) -> Result<S::Ok, S::Error>
157    where
158        S: Serializer,
159    {
160        method.as_str().serialize(serializer)
161    }
162
163    pub fn deserialize<'de, D>(deserializer: D) -> Result<Method, D::Error>
164    where
165        D: Deserializer<'de>,
166    {
167        let s = String::deserialize(deserializer)?;
168        Method::from_bytes(s.as_bytes()).map_err(serde::de::Error::custom)
169    }
170}
171
172/// Stateful response handler
173pub struct StatefulResponseHandler {
174    /// State machine manager
175    state_manager: Arc<StateMachineManager>,
176    /// Stateful configurations by path pattern
177    configs: Arc<RwLock<HashMap<String, StatefulConfig>>>,
178}
179
180impl StatefulResponseHandler {
181    /// Create a new stateful response handler
182    pub fn new() -> Result<Self> {
183        Ok(Self {
184            state_manager: Arc::new(StateMachineManager::new()),
185            configs: Arc::new(RwLock::new(HashMap::new())),
186        })
187    }
188
189    /// Add a stateful configuration for a path
190    pub async fn add_config(&self, path_pattern: String, config: StatefulConfig) {
191        let mut configs = self.configs.write().await;
192        configs.insert(path_pattern, config);
193    }
194
195    /// Check if this handler can process the request
196    pub async fn can_handle(&self, _method: &Method, path: &str) -> bool {
197        let configs = self.configs.read().await;
198        for (pattern, _) in configs.iter() {
199            if self.path_matches(pattern, path) {
200                return true;
201            }
202        }
203        false
204    }
205
206    /// Process a request and return stateful response if applicable
207    pub async fn process_request(
208        &self,
209        method: &Method,
210        uri: &Uri,
211        headers: &HeaderMap,
212        body: Option<&[u8]>,
213    ) -> Result<Option<StatefulResponse>> {
214        let path = uri.path();
215
216        // Find matching configuration
217        let config = {
218            let configs = self.configs.read().await;
219            configs
220                .iter()
221                .find(|(pattern, _)| self.path_matches(pattern, path))
222                .map(|(_, config)| config.clone())
223        };
224
225        let config = match config {
226            Some(c) => c,
227            None => return Ok(None),
228        };
229
230        // Extract resource ID
231        let resource_id =
232            self.extract_resource_id(&config.resource_id_extract, uri, headers, body)?;
233
234        // Get or create state instance
235        let state_instance = self
236            .state_manager
237            .get_or_create_instance(
238                resource_id.clone(),
239                config.resource_type.clone(),
240                "initial".to_string(), // Default initial state
241            )
242            .await?;
243
244        // Check for transition triggers
245        let new_state = self
246            .check_transitions(&config, method, path, &state_instance, headers, body)
247            .await?;
248
249        // Get current state (after potential transition)
250        let current_state = if let Some(ref state) = new_state {
251            state.clone()
252        } else {
253            state_instance.current_state.clone()
254        };
255
256        // Generate response based on current state
257        let state_response = config.state_responses.get(&current_state).ok_or_else(|| {
258            Error::generic(format!("No response configuration for state '{}'", current_state))
259        })?;
260
261        // Update state instance if transition occurred
262        if let Some(ref new_state) = new_state {
263            let mut updated_instance = state_instance.clone();
264            updated_instance.transition_to(new_state.clone());
265            self.state_manager
266                .update_instance(resource_id.clone(), updated_instance)
267                .await?;
268        }
269
270        Ok(Some(StatefulResponse {
271            status_code: state_response.status_code,
272            headers: state_response.headers.clone(),
273            body: self.render_body_template(&state_response.body_template, &state_instance)?,
274            content_type: state_response.content_type.clone(),
275            state: current_state,
276            resource_id: resource_id.clone(),
277        }))
278    }
279
280    /// Extract resource ID from request
281    fn extract_resource_id(
282        &self,
283        extract: &ResourceIdExtract,
284        uri: &Uri,
285        headers: &HeaderMap,
286        body: Option<&[u8]>,
287    ) -> Result<String> {
288        let path = uri.path();
289        match extract {
290            ResourceIdExtract::PathParam { param } => {
291                // Extract from path (e.g., "/orders/123" with pattern "/orders/{order_id}" -> "123")
292                // Simple implementation: extract last segment or use regex
293                let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
294                if let Some(last) = segments.last() {
295                    Ok(last.to_string())
296                } else {
297                    Err(Error::generic(format!(
298                        "Could not extract path parameter '{}' from path '{}'",
299                        param, path
300                    )))
301                }
302            }
303            ResourceIdExtract::Header { name } => headers
304                .get(name)
305                .and_then(|v| v.to_str().ok())
306                .map(|s| s.to_string())
307                .ok_or_else(|| Error::generic(format!("Header '{}' not found", name))),
308            ResourceIdExtract::QueryParam { param } => {
309                // Extract from query string
310                uri.query()
311                    .and_then(|q| {
312                        url::form_urlencoded::parse(q.as_bytes())
313                            .find(|(k, _)| k == param)
314                            .map(|(_, v)| v.to_string())
315                    })
316                    .ok_or_else(|| Error::generic(format!("Query parameter '{}' not found", param)))
317            }
318            ResourceIdExtract::JsonPath { path: json_path } => {
319                let body_str = body
320                    .and_then(|b| std::str::from_utf8(b).ok())
321                    .ok_or_else(|| Error::generic("Request body is not valid UTF-8".to_string()))?;
322
323                let json: Value = serde_json::from_str(body_str)
324                    .map_err(|e| Error::generic(format!("Invalid JSON body: {}", e)))?;
325
326                // Simple JSONPath implementation (supports $.field notation)
327                self.extract_json_path(&json, json_path)
328            }
329            ResourceIdExtract::Composite { extractors } => {
330                // Try each extractor in order
331                for extract in extractors {
332                    if let Ok(id) = self.extract_resource_id(extract, uri, headers, body) {
333                        return Ok(id);
334                    }
335                }
336                Err(Error::generic("Could not extract resource ID from any source".to_string()))
337            }
338        }
339    }
340
341    /// Extract value from JSON using simple JSONPath
342    fn extract_json_path(&self, json: &Value, path: &str) -> Result<String> {
343        let path = path.trim_start_matches('$').trim_start_matches('.');
344        let parts: Vec<&str> = path.split('.').collect();
345
346        let mut current = json;
347        for part in parts {
348            match current {
349                Value::Object(map) => {
350                    current = map
351                        .get(part)
352                        .ok_or_else(|| Error::generic(format!("Path '{}' not found", path)))?;
353                }
354                Value::Array(arr) => {
355                    let idx: usize = part
356                        .parse()
357                        .map_err(|_| Error::generic(format!("Invalid array index: {}", part)))?;
358                    current = arr.get(idx).ok_or_else(|| {
359                        Error::generic(format!("Array index {} out of bounds", idx))
360                    })?;
361                }
362                _ => {
363                    return Err(Error::generic(format!(
364                        "Cannot traverse path '{}' at '{}'",
365                        path, part
366                    )));
367                }
368            }
369        }
370
371        match current {
372            Value::String(s) => Ok(s.clone()),
373            Value::Number(n) => Ok(n.to_string()),
374            _ => {
375                Err(Error::generic(format!("Path '{}' does not point to a string or number", path)))
376            }
377        }
378    }
379
380    /// Check for transition triggers
381    async fn check_transitions(
382        &self,
383        config: &StatefulConfig,
384        method: &Method,
385        path: &str,
386        instance: &StateInstance,
387        headers: &HeaderMap,
388        body: Option<&[u8]>,
389    ) -> Result<Option<String>> {
390        for transition in &config.transitions {
391            // Check if method and path match
392            if transition.method != *method {
393                continue;
394            }
395
396            if !self.path_matches(&transition.path_pattern, path) {
397                continue;
398            }
399
400            // Check if current state matches
401            if instance.current_state != transition.from_state {
402                continue;
403            }
404
405            // Check condition if present
406            if let Some(ref condition) = transition.condition {
407                if !self.evaluate_condition(condition, headers, body)? {
408                    continue;
409                }
410            }
411
412            // Transition matches!
413            debug!(
414                "State transition triggered: {} -> {} for resource {}",
415                transition.from_state, transition.to_state, instance.resource_id
416            );
417
418            return Ok(Some(transition.to_state.clone()));
419        }
420
421        Ok(None)
422    }
423
424    /// Evaluate a condition expression
425    fn evaluate_condition(
426        &self,
427        condition: &str,
428        _headers: &HeaderMap,
429        body: Option<&[u8]>,
430    ) -> Result<bool> {
431        // Simple condition evaluation (can be enhanced with Rhai later)
432        // For now, support basic JSONPath expressions on body
433        if condition.starts_with("$.") {
434            let body_str = body
435                .and_then(|b| std::str::from_utf8(b).ok())
436                .ok_or_else(|| Error::generic("Request body is not valid UTF-8".to_string()))?;
437
438            let json: Value = serde_json::from_str(body_str)
439                .map_err(|e| Error::generic(format!("Invalid JSON body: {}", e)))?;
440
441            // Extract value and check if it's truthy
442            let value = self.extract_json_path(&json, condition)?;
443            Ok(!value.is_empty() && value != "false" && value != "0")
444        } else {
445            // Default: condition is true if present
446            Ok(true)
447        }
448    }
449
450    /// Render body template with state data
451    fn render_body_template(&self, template: &str, instance: &StateInstance) -> Result<String> {
452        let mut result = template.to_string();
453
454        // Replace {{state}} with current state
455        result = result.replace("{{state}}", &instance.current_state);
456
457        // Replace {{resource_id}} with resource ID
458        result = result.replace("{{resource_id}}", &instance.resource_id);
459
460        // Replace state data variables {{state_data.key}}
461        for (key, value) in &instance.state_data {
462            let placeholder = format!("{{{{state_data.{}}}}}", key);
463            let value_str = match value {
464                Value::String(s) => s.clone(),
465                Value::Number(n) => n.to_string(),
466                Value::Bool(b) => b.to_string(),
467                _ => serde_json::to_string(value).unwrap_or_default(),
468            };
469            result = result.replace(&placeholder, &value_str);
470        }
471
472        Ok(result)
473    }
474
475    /// Process a stub with state machine configuration
476    ///
477    /// This method extracts resource ID, manages state, and returns state information
478    /// that can be used to select or modify stub responses based on current state.
479    ///
480    /// Returns:
481    /// - `Ok(Some(StateInfo))` if state machine config exists and state was processed
482    /// - `Ok(None)` if no state machine config or state processing not applicable
483    /// - `Err` if there was an error processing state
484    pub async fn process_stub_state(
485        &self,
486        method: &Method,
487        uri: &Uri,
488        headers: &HeaderMap,
489        body: Option<&[u8]>,
490        resource_type: &str,
491        resource_id_extract: &ResourceIdExtract,
492        initial_state: &str,
493        transitions: Option<&[TransitionTrigger]>,
494    ) -> Result<Option<StateInfo>> {
495        // Extract resource ID
496        let resource_id = self.extract_resource_id(resource_id_extract, uri, headers, body)?;
497
498        // Get or create state instance
499        let state_instance = self
500            .state_manager
501            .get_or_create_instance(
502                resource_id.clone(),
503                resource_type.to_string(),
504                initial_state.to_string(),
505            )
506            .await?;
507
508        // Check for transition triggers if provided
509        let new_state = if let Some(transition_list) = transitions {
510            let path = uri.path();
511            // Create a temporary config-like structure for transition checking
512            // We'll check transitions manually since we don't have a full StatefulConfig
513            let mut transitioned_state = None;
514
515            for transition in transition_list {
516                // Check if method and path match
517                if transition.method != *method {
518                    continue;
519                }
520
521                if !self.path_matches(&transition.path_pattern, path) {
522                    continue;
523                }
524
525                // Check if current state matches
526                if state_instance.current_state != transition.from_state {
527                    continue;
528                }
529
530                // Check condition if present
531                if let Some(ref condition) = transition.condition {
532                    if !self.evaluate_condition(condition, headers, body)? {
533                        continue;
534                    }
535                }
536
537                // Transition matches!
538                debug!(
539                    "State transition triggered in stub processing: {} -> {} for resource {}",
540                    transition.from_state, transition.to_state, resource_id
541                );
542
543                transitioned_state = Some(transition.to_state.clone());
544                break; // Use first matching transition
545            }
546
547            transitioned_state
548        } else {
549            None
550        };
551
552        // Update state if transition occurred
553        let final_state = if let Some(ref new_state) = new_state {
554            let mut updated_instance = state_instance.clone();
555            updated_instance.transition_to(new_state.clone());
556            self.state_manager
557                .update_instance(resource_id.clone(), updated_instance)
558                .await?;
559            new_state.clone()
560        } else {
561            state_instance.current_state.clone()
562        };
563
564        Ok(Some(StateInfo {
565            resource_id: resource_id.clone(),
566            current_state: final_state,
567            state_data: state_instance.state_data.clone(),
568        }))
569    }
570
571    /// Update state for a resource (for use with stub transitions)
572    pub async fn update_resource_state(
573        &self,
574        resource_id: &str,
575        resource_type: &str,
576        new_state: &str,
577    ) -> Result<()> {
578        let mut instances = self.state_manager.instances.write().await;
579        if let Some(instance) = instances.get_mut(resource_id) {
580            if instance.resource_type == resource_type {
581                instance.transition_to(new_state.to_string());
582                return Ok(());
583            }
584        }
585        Err(Error::generic(format!(
586            "Resource '{}' of type '{}' not found",
587            resource_id, resource_type
588        )))
589    }
590
591    /// Get current state for a resource
592    pub async fn get_resource_state(
593        &self,
594        resource_id: &str,
595        resource_type: &str,
596    ) -> Result<Option<StateInfo>> {
597        let instances = self.state_manager.instances.read().await;
598        if let Some(instance) = instances.get(resource_id) {
599            if instance.resource_type == resource_type {
600                return Ok(Some(StateInfo {
601                    resource_id: resource_id.to_string(),
602                    current_state: instance.current_state.clone(),
603                    state_data: instance.state_data.clone(),
604                }));
605            }
606        }
607        Ok(None)
608    }
609
610    /// Check if path matches pattern (simple wildcard matching)
611    fn path_matches(&self, pattern: &str, path: &str) -> bool {
612        // Simple pattern matching: support {param} and * wildcards
613        let pattern_regex = pattern.replace("{", "(?P<").replace("}", ">[^/]+)").replace("*", ".*");
614        let regex = regex::Regex::new(&format!("^{}$", pattern_regex));
615        match regex {
616            Ok(re) => re.is_match(path),
617            Err(_) => pattern == path, // Fallback to exact match
618        }
619    }
620}
621
622/// State information for stub response selection
623#[derive(Debug, Clone)]
624pub struct StateInfo {
625    /// Resource ID
626    pub resource_id: String,
627    /// Current state name
628    pub current_state: String,
629    /// State data (key-value pairs)
630    pub state_data: HashMap<String, Value>,
631}
632
633/// Stateful response
634#[derive(Debug, Clone)]
635pub struct StatefulResponse {
636    /// HTTP status code
637    pub status_code: u16,
638    /// Response headers
639    pub headers: HashMap<String, String>,
640    /// Response body
641    pub body: String,
642    /// Content type
643    pub content_type: String,
644    /// Current state
645    pub state: String,
646    /// Resource ID
647    pub resource_id: String,
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653
654    // =========================================================================
655    // StateInstance tests
656    // =========================================================================
657
658    #[test]
659    fn test_state_instance_new() {
660        let instance =
661            StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
662        assert_eq!(instance.resource_id, "order-123");
663        assert_eq!(instance.resource_type, "order");
664        assert_eq!(instance.current_state, "pending");
665        assert!(instance.state_data.is_empty());
666    }
667
668    #[test]
669    fn test_state_instance_transition_to() {
670        let mut instance =
671            StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
672        instance.transition_to("confirmed".to_string());
673        assert_eq!(instance.current_state, "confirmed");
674
675        instance.transition_to("shipped".to_string());
676        assert_eq!(instance.current_state, "shipped");
677    }
678
679    #[test]
680    fn test_state_instance_clone() {
681        let instance =
682            StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
683        let cloned = instance.clone();
684        assert_eq!(cloned.resource_id, instance.resource_id);
685        assert_eq!(cloned.current_state, instance.current_state);
686    }
687
688    #[test]
689    fn test_state_instance_debug() {
690        let instance =
691            StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
692        let debug_str = format!("{:?}", instance);
693        assert!(debug_str.contains("order-123"));
694        assert!(debug_str.contains("pending"));
695    }
696
697    // =========================================================================
698    // StateMachineManager tests
699    // =========================================================================
700
701    #[tokio::test]
702    async fn test_state_machine_manager_new() {
703        let manager = StateMachineManager::new();
704        let instances = manager.instances.read().await;
705        assert!(instances.is_empty());
706    }
707
708    #[tokio::test]
709    async fn test_state_machine_manager_get_or_create_new() {
710        let manager = StateMachineManager::new();
711        let instance = manager
712            .get_or_create_instance(
713                "order-123".to_string(),
714                "order".to_string(),
715                "pending".to_string(),
716            )
717            .await
718            .unwrap();
719        assert_eq!(instance.resource_id, "order-123");
720        assert_eq!(instance.current_state, "pending");
721    }
722
723    #[tokio::test]
724    async fn test_state_machine_manager_get_or_create_existing() {
725        let manager = StateMachineManager::new();
726
727        // Create initial instance
728        let instance1 = manager
729            .get_or_create_instance(
730                "order-123".to_string(),
731                "order".to_string(),
732                "pending".to_string(),
733            )
734            .await
735            .unwrap();
736        assert_eq!(instance1.current_state, "pending");
737
738        // Get the same instance - should return existing with same state
739        let instance2 = manager
740            .get_or_create_instance(
741                "order-123".to_string(),
742                "order".to_string(),
743                "confirmed".to_string(), // Different initial state - should be ignored
744            )
745            .await
746            .unwrap();
747        assert_eq!(instance2.current_state, "pending"); // Still pending
748    }
749
750    #[tokio::test]
751    async fn test_state_machine_manager_update_instance() {
752        let manager = StateMachineManager::new();
753
754        // Create initial instance
755        let mut instance = manager
756            .get_or_create_instance(
757                "order-123".to_string(),
758                "order".to_string(),
759                "pending".to_string(),
760            )
761            .await
762            .unwrap();
763
764        // Update state
765        instance.transition_to("confirmed".to_string());
766        manager.update_instance("order-123".to_string(), instance).await.unwrap();
767
768        // Verify update
769        let updated = manager
770            .get_or_create_instance(
771                "order-123".to_string(),
772                "order".to_string(),
773                "pending".to_string(),
774            )
775            .await
776            .unwrap();
777        assert_eq!(updated.current_state, "confirmed");
778    }
779
780    // =========================================================================
781    // StatefulConfig tests
782    // =========================================================================
783
784    #[test]
785    fn test_stateful_config_serialize_deserialize() {
786        let config = StatefulConfig {
787            resource_id_extract: ResourceIdExtract::PathParam {
788                param: "order_id".to_string(),
789            },
790            resource_type: "order".to_string(),
791            state_responses: {
792                let mut map = HashMap::new();
793                map.insert(
794                    "pending".to_string(),
795                    StateResponse {
796                        status_code: 200,
797                        headers: HashMap::new(),
798                        body_template: "{\"status\": \"pending\"}".to_string(),
799                        content_type: "application/json".to_string(),
800                    },
801                );
802                map
803            },
804            transitions: vec![],
805        };
806
807        let json = serde_json::to_string(&config).unwrap();
808        let deserialized: StatefulConfig = serde_json::from_str(&json).unwrap();
809        assert_eq!(deserialized.resource_type, "order");
810    }
811
812    #[test]
813    fn test_stateful_config_debug() {
814        let config = StatefulConfig {
815            resource_id_extract: ResourceIdExtract::PathParam {
816                param: "order_id".to_string(),
817            },
818            resource_type: "order".to_string(),
819            state_responses: HashMap::new(),
820            transitions: vec![],
821        };
822        let debug_str = format!("{:?}", config);
823        assert!(debug_str.contains("order"));
824    }
825
826    #[test]
827    fn test_stateful_config_clone() {
828        let config = StatefulConfig {
829            resource_id_extract: ResourceIdExtract::PathParam {
830                param: "id".to_string(),
831            },
832            resource_type: "user".to_string(),
833            state_responses: HashMap::new(),
834            transitions: vec![],
835        };
836        let cloned = config.clone();
837        assert_eq!(cloned.resource_type, "user");
838    }
839
840    // =========================================================================
841    // ResourceIdExtract tests
842    // =========================================================================
843
844    #[test]
845    fn test_resource_id_extract_path_param() {
846        let extract = ResourceIdExtract::PathParam {
847            param: "order_id".to_string(),
848        };
849        let json = serde_json::to_string(&extract).unwrap();
850        assert!(json.contains("path_param"));
851    }
852
853    #[test]
854    fn test_resource_id_extract_json_path() {
855        let extract = ResourceIdExtract::JsonPath {
856            path: "$.order.id".to_string(),
857        };
858        let json = serde_json::to_string(&extract).unwrap();
859        assert!(json.contains("json_path"));
860    }
861
862    #[test]
863    fn test_resource_id_extract_header() {
864        let extract = ResourceIdExtract::Header {
865            name: "X-Order-ID".to_string(),
866        };
867        let json = serde_json::to_string(&extract).unwrap();
868        assert!(json.contains("header"));
869    }
870
871    #[test]
872    fn test_resource_id_extract_query_param() {
873        let extract = ResourceIdExtract::QueryParam {
874            param: "order_id".to_string(),
875        };
876        let json = serde_json::to_string(&extract).unwrap();
877        assert!(json.contains("query_param"));
878    }
879
880    #[test]
881    fn test_resource_id_extract_composite() {
882        let extract = ResourceIdExtract::Composite {
883            extractors: vec![
884                ResourceIdExtract::PathParam {
885                    param: "id".to_string(),
886                },
887                ResourceIdExtract::Header {
888                    name: "X-ID".to_string(),
889                },
890            ],
891        };
892        let json = serde_json::to_string(&extract).unwrap();
893        assert!(json.contains("composite"));
894    }
895
896    // =========================================================================
897    // StateResponse tests
898    // =========================================================================
899
900    #[test]
901    fn test_state_response_serialize_deserialize() {
902        let response = StateResponse {
903            status_code: 200,
904            headers: {
905                let mut h = HashMap::new();
906                h.insert("X-State".to_string(), "pending".to_string());
907                h
908            },
909            body_template: "{\"state\": \"{{state}}\"}".to_string(),
910            content_type: "application/json".to_string(),
911        };
912        let json = serde_json::to_string(&response).unwrap();
913        let deserialized: StateResponse = serde_json::from_str(&json).unwrap();
914        assert_eq!(deserialized.status_code, 200);
915        assert_eq!(deserialized.content_type, "application/json");
916    }
917
918    #[test]
919    fn test_state_response_clone() {
920        let response = StateResponse {
921            status_code: 201,
922            headers: HashMap::new(),
923            body_template: "{}".to_string(),
924            content_type: "text/plain".to_string(),
925        };
926        let cloned = response.clone();
927        assert_eq!(cloned.status_code, 201);
928    }
929
930    #[test]
931    fn test_state_response_debug() {
932        let response = StateResponse {
933            status_code: 404,
934            headers: HashMap::new(),
935            body_template: "Not found".to_string(),
936            content_type: "text/plain".to_string(),
937        };
938        let debug_str = format!("{:?}", response);
939        assert!(debug_str.contains("404"));
940    }
941
942    // =========================================================================
943    // TransitionTrigger tests
944    // =========================================================================
945
946    #[test]
947    fn test_transition_trigger_serialize_deserialize() {
948        let trigger = TransitionTrigger {
949            method: Method::POST,
950            path_pattern: "/orders/{id}/confirm".to_string(),
951            from_state: "pending".to_string(),
952            to_state: "confirmed".to_string(),
953            condition: None,
954        };
955        let json = serde_json::to_string(&trigger).unwrap();
956        let deserialized: TransitionTrigger = serde_json::from_str(&json).unwrap();
957        assert_eq!(deserialized.from_state, "pending");
958        assert_eq!(deserialized.to_state, "confirmed");
959    }
960
961    #[test]
962    fn test_transition_trigger_with_condition() {
963        let trigger = TransitionTrigger {
964            method: Method::POST,
965            path_pattern: "/orders/{id}/ship".to_string(),
966            from_state: "confirmed".to_string(),
967            to_state: "shipped".to_string(),
968            condition: Some("$.payment.verified".to_string()),
969        };
970        let json = serde_json::to_string(&trigger).unwrap();
971        assert!(json.contains("payment.verified"));
972    }
973
974    #[test]
975    fn test_transition_trigger_clone() {
976        let trigger = TransitionTrigger {
977            method: Method::DELETE,
978            path_pattern: "/orders/{id}".to_string(),
979            from_state: "pending".to_string(),
980            to_state: "cancelled".to_string(),
981            condition: None,
982        };
983        let cloned = trigger.clone();
984        assert_eq!(cloned.method, Method::DELETE);
985    }
986
987    // =========================================================================
988    // StatefulResponseHandler tests
989    // =========================================================================
990
991    #[tokio::test]
992    async fn test_stateful_response_handler_new() {
993        let handler = StatefulResponseHandler::new().unwrap();
994        let configs = handler.configs.read().await;
995        assert!(configs.is_empty());
996    }
997
998    #[tokio::test]
999    async fn test_stateful_response_handler_add_config() {
1000        let handler = StatefulResponseHandler::new().unwrap();
1001        let config = StatefulConfig {
1002            resource_id_extract: ResourceIdExtract::PathParam {
1003                param: "id".to_string(),
1004            },
1005            resource_type: "order".to_string(),
1006            state_responses: HashMap::new(),
1007            transitions: vec![],
1008        };
1009
1010        handler.add_config("/orders/{id}".to_string(), config).await;
1011
1012        let configs = handler.configs.read().await;
1013        assert!(configs.contains_key("/orders/{id}"));
1014    }
1015
1016    #[tokio::test]
1017    async fn test_stateful_response_handler_can_handle_true() {
1018        let handler = StatefulResponseHandler::new().unwrap();
1019        let config = StatefulConfig {
1020            resource_id_extract: ResourceIdExtract::PathParam {
1021                param: "id".to_string(),
1022            },
1023            resource_type: "order".to_string(),
1024            state_responses: HashMap::new(),
1025            transitions: vec![],
1026        };
1027
1028        handler.add_config("/orders/{id}".to_string(), config).await;
1029
1030        assert!(handler.can_handle(&Method::GET, "/orders/123").await);
1031    }
1032
1033    #[tokio::test]
1034    async fn test_stateful_response_handler_can_handle_false() {
1035        let handler = StatefulResponseHandler::new().unwrap();
1036        assert!(!handler.can_handle(&Method::GET, "/orders/123").await);
1037    }
1038
1039    // =========================================================================
1040    // Path matching tests
1041    // =========================================================================
1042
1043    #[test]
1044    fn test_path_matching() {
1045        let handler = StatefulResponseHandler::new().unwrap();
1046
1047        assert!(handler.path_matches("/orders/{id}", "/orders/123"));
1048        assert!(handler.path_matches("/api/*", "/api/users"));
1049        assert!(!handler.path_matches("/orders/{id}", "/orders/123/items"));
1050    }
1051
1052    #[test]
1053    fn test_path_matching_exact() {
1054        let handler = StatefulResponseHandler::new().unwrap();
1055        assert!(handler.path_matches("/api/health", "/api/health"));
1056        assert!(!handler.path_matches("/api/health", "/api/health/check"));
1057    }
1058
1059    #[test]
1060    fn test_path_matching_multiple_params() {
1061        let handler = StatefulResponseHandler::new().unwrap();
1062        assert!(handler.path_matches("/users/{user_id}/orders/{order_id}", "/users/1/orders/2"));
1063    }
1064
1065    #[test]
1066    fn test_path_matching_wildcard() {
1067        let handler = StatefulResponseHandler::new().unwrap();
1068        assert!(handler.path_matches("/api/*", "/api/anything"));
1069        assert!(handler.path_matches("/api/*", "/api/users/123"));
1070    }
1071
1072    // =========================================================================
1073    // Resource ID extraction tests
1074    // =========================================================================
1075
1076    #[tokio::test]
1077    async fn test_extract_resource_id_from_path() {
1078        let handler = StatefulResponseHandler::new().unwrap();
1079        let extract = ResourceIdExtract::PathParam {
1080            param: "order_id".to_string(),
1081        };
1082        let uri: Uri = "/orders/12345".parse().unwrap();
1083        let headers = HeaderMap::new();
1084
1085        let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1086        assert_eq!(id, "12345");
1087    }
1088
1089    #[tokio::test]
1090    async fn test_extract_resource_id_from_header() {
1091        let handler = StatefulResponseHandler::new().unwrap();
1092        let extract = ResourceIdExtract::Header {
1093            name: "x-order-id".to_string(),
1094        };
1095        let uri: Uri = "/orders".parse().unwrap();
1096        let mut headers = HeaderMap::new();
1097        headers.insert("x-order-id", "order-abc".parse().unwrap());
1098
1099        let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1100        assert_eq!(id, "order-abc");
1101    }
1102
1103    #[tokio::test]
1104    async fn test_extract_resource_id_from_query_param() {
1105        let handler = StatefulResponseHandler::new().unwrap();
1106        let extract = ResourceIdExtract::QueryParam {
1107            param: "id".to_string(),
1108        };
1109        let uri: Uri = "/orders?id=query-123".parse().unwrap();
1110        let headers = HeaderMap::new();
1111
1112        let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1113        assert_eq!(id, "query-123");
1114    }
1115
1116    #[tokio::test]
1117    async fn test_extract_resource_id_from_json_body() {
1118        let handler = StatefulResponseHandler::new().unwrap();
1119        let extract = ResourceIdExtract::JsonPath {
1120            path: "$.order.id".to_string(),
1121        };
1122        let uri: Uri = "/orders".parse().unwrap();
1123        let headers = HeaderMap::new();
1124        let body = br#"{"order": {"id": "json-456"}}"#;
1125
1126        let id = handler.extract_resource_id(&extract, &uri, &headers, Some(body)).unwrap();
1127        assert_eq!(id, "json-456");
1128    }
1129
1130    #[tokio::test]
1131    async fn test_extract_resource_id_composite() {
1132        let handler = StatefulResponseHandler::new().unwrap();
1133        let extract = ResourceIdExtract::Composite {
1134            extractors: vec![
1135                ResourceIdExtract::Header {
1136                    name: "x-id".to_string(),
1137                },
1138                ResourceIdExtract::PathParam {
1139                    param: "id".to_string(),
1140                },
1141            ],
1142        };
1143        let uri: Uri = "/orders/fallback-123".parse().unwrap();
1144        let headers = HeaderMap::new(); // No header, should fall back to path
1145
1146        let id = handler.extract_resource_id(&extract, &uri, &headers, None).unwrap();
1147        assert_eq!(id, "fallback-123");
1148    }
1149
1150    #[tokio::test]
1151    async fn test_extract_resource_id_header_not_found() {
1152        let handler = StatefulResponseHandler::new().unwrap();
1153        let extract = ResourceIdExtract::Header {
1154            name: "x-missing".to_string(),
1155        };
1156        let uri: Uri = "/orders".parse().unwrap();
1157        let headers = HeaderMap::new();
1158
1159        let result = handler.extract_resource_id(&extract, &uri, &headers, None);
1160        assert!(result.is_err());
1161    }
1162
1163    // =========================================================================
1164    // JSON path extraction tests
1165    // =========================================================================
1166
1167    #[test]
1168    fn test_extract_json_path_simple() {
1169        let handler = StatefulResponseHandler::new().unwrap();
1170        let json: Value = serde_json::json!({"id": "123"});
1171        let result = handler.extract_json_path(&json, "$.id").unwrap();
1172        assert_eq!(result, "123");
1173    }
1174
1175    #[test]
1176    fn test_extract_json_path_nested() {
1177        let handler = StatefulResponseHandler::new().unwrap();
1178        let json: Value = serde_json::json!({"order": {"details": {"id": "456"}}});
1179        let result = handler.extract_json_path(&json, "$.order.details.id").unwrap();
1180        assert_eq!(result, "456");
1181    }
1182
1183    #[test]
1184    fn test_extract_json_path_number() {
1185        let handler = StatefulResponseHandler::new().unwrap();
1186        let json: Value = serde_json::json!({"count": 42});
1187        let result = handler.extract_json_path(&json, "$.count").unwrap();
1188        assert_eq!(result, "42");
1189    }
1190
1191    #[test]
1192    fn test_extract_json_path_array_index() {
1193        let handler = StatefulResponseHandler::new().unwrap();
1194        let json: Value = serde_json::json!({"items": ["a", "b", "c"]});
1195        let result = handler.extract_json_path(&json, "$.items.1").unwrap();
1196        assert_eq!(result, "b");
1197    }
1198
1199    #[test]
1200    fn test_extract_json_path_not_found() {
1201        let handler = StatefulResponseHandler::new().unwrap();
1202        let json: Value = serde_json::json!({"other": "value"});
1203        let result = handler.extract_json_path(&json, "$.missing");
1204        assert!(result.is_err());
1205    }
1206
1207    // =========================================================================
1208    // Body template rendering tests
1209    // =========================================================================
1210
1211    #[test]
1212    fn test_render_body_template_state() {
1213        let handler = StatefulResponseHandler::new().unwrap();
1214        let instance =
1215            StateInstance::new("order-123".to_string(), "order".to_string(), "pending".to_string());
1216        let template = r#"{"status": "{{state}}"}"#;
1217        let result = handler.render_body_template(template, &instance).unwrap();
1218        assert_eq!(result, r#"{"status": "pending"}"#);
1219    }
1220
1221    #[test]
1222    fn test_render_body_template_resource_id() {
1223        let handler = StatefulResponseHandler::new().unwrap();
1224        let instance =
1225            StateInstance::new("order-456".to_string(), "order".to_string(), "shipped".to_string());
1226        let template = r#"{"id": "{{resource_id}}"}"#;
1227        let result = handler.render_body_template(template, &instance).unwrap();
1228        assert_eq!(result, r#"{"id": "order-456"}"#);
1229    }
1230
1231    #[test]
1232    fn test_render_body_template_state_data() {
1233        let handler = StatefulResponseHandler::new().unwrap();
1234        let mut instance =
1235            StateInstance::new("order-789".to_string(), "order".to_string(), "shipped".to_string());
1236        instance
1237            .state_data
1238            .insert("carrier".to_string(), Value::String("FedEx".to_string()));
1239        let template = r#"{"carrier": "{{state_data.carrier}}"}"#;
1240        let result = handler.render_body_template(template, &instance).unwrap();
1241        assert_eq!(result, r#"{"carrier": "FedEx"}"#);
1242    }
1243
1244    #[test]
1245    fn test_render_body_template_multiple_placeholders() {
1246        let handler = StatefulResponseHandler::new().unwrap();
1247        let instance = StateInstance::new(
1248            "order-abc".to_string(),
1249            "order".to_string(),
1250            "confirmed".to_string(),
1251        );
1252        let template = r#"{"id": "{{resource_id}}", "status": "{{state}}"}"#;
1253        let result = handler.render_body_template(template, &instance).unwrap();
1254        assert_eq!(result, r#"{"id": "order-abc", "status": "confirmed"}"#);
1255    }
1256
1257    // =========================================================================
1258    // Process request tests
1259    // =========================================================================
1260
1261    #[tokio::test]
1262    async fn test_process_request_no_config() {
1263        let handler = StatefulResponseHandler::new().unwrap();
1264        let uri: Uri = "/orders/123".parse().unwrap();
1265        let headers = HeaderMap::new();
1266
1267        let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1268        assert!(result.is_none());
1269    }
1270
1271    #[tokio::test]
1272    async fn test_process_request_with_config() {
1273        let handler = StatefulResponseHandler::new().unwrap();
1274        let mut state_responses = HashMap::new();
1275        state_responses.insert(
1276            "initial".to_string(),
1277            StateResponse {
1278                status_code: 200,
1279                headers: HashMap::new(),
1280                body_template: r#"{"state": "{{state}}", "id": "{{resource_id}}"}"#.to_string(),
1281                content_type: "application/json".to_string(),
1282            },
1283        );
1284
1285        let config = StatefulConfig {
1286            resource_id_extract: ResourceIdExtract::PathParam {
1287                param: "id".to_string(),
1288            },
1289            resource_type: "order".to_string(),
1290            state_responses,
1291            transitions: vec![],
1292        };
1293
1294        handler.add_config("/orders/{id}".to_string(), config).await;
1295
1296        let uri: Uri = "/orders/test-123".parse().unwrap();
1297        let headers = HeaderMap::new();
1298
1299        let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1300        assert!(result.is_some());
1301
1302        let response = result.unwrap();
1303        assert_eq!(response.status_code, 200);
1304        assert_eq!(response.state, "initial");
1305        assert_eq!(response.resource_id, "test-123");
1306        assert!(response.body.contains("test-123"));
1307    }
1308
1309    #[tokio::test]
1310    async fn test_process_request_with_transition() {
1311        let handler = StatefulResponseHandler::new().unwrap();
1312        let mut state_responses = HashMap::new();
1313        state_responses.insert(
1314            "initial".to_string(),
1315            StateResponse {
1316                status_code: 200,
1317                headers: HashMap::new(),
1318                body_template: r#"{"state": "{{state}}"}"#.to_string(),
1319                content_type: "application/json".to_string(),
1320            },
1321        );
1322        state_responses.insert(
1323            "confirmed".to_string(),
1324            StateResponse {
1325                status_code: 200,
1326                headers: HashMap::new(),
1327                body_template: r#"{"state": "{{state}}"}"#.to_string(),
1328                content_type: "application/json".to_string(),
1329            },
1330        );
1331
1332        let config = StatefulConfig {
1333            resource_id_extract: ResourceIdExtract::PathParam {
1334                param: "id".to_string(),
1335            },
1336            resource_type: "order".to_string(),
1337            state_responses,
1338            transitions: vec![TransitionTrigger {
1339                method: Method::POST,
1340                path_pattern: "/orders/{id}".to_string(),
1341                from_state: "initial".to_string(),
1342                to_state: "confirmed".to_string(),
1343                condition: None,
1344            }],
1345        };
1346
1347        handler.add_config("/orders/{id}".to_string(), config).await;
1348
1349        // First request - should be in initial state
1350        let uri: Uri = "/orders/order-1".parse().unwrap();
1351        let headers = HeaderMap::new();
1352
1353        let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1354        assert_eq!(result.unwrap().state, "initial");
1355
1356        // POST to trigger transition
1357        let result = handler.process_request(&Method::POST, &uri, &headers, None).await.unwrap();
1358        assert_eq!(result.unwrap().state, "confirmed");
1359
1360        // Subsequent GET should show confirmed state
1361        let result = handler.process_request(&Method::GET, &uri, &headers, None).await.unwrap();
1362        assert_eq!(result.unwrap().state, "confirmed");
1363    }
1364
1365    // =========================================================================
1366    // Stub state processing tests
1367    // =========================================================================
1368
1369    #[tokio::test]
1370    async fn test_process_stub_state() {
1371        let handler = StatefulResponseHandler::new().unwrap();
1372        let extract = ResourceIdExtract::PathParam {
1373            param: "id".to_string(),
1374        };
1375        let uri: Uri = "/users/user-123".parse().unwrap();
1376        let headers = HeaderMap::new();
1377
1378        let result = handler
1379            .process_stub_state(
1380                &Method::GET,
1381                &uri,
1382                &headers,
1383                None,
1384                "user",
1385                &extract,
1386                "active",
1387                None,
1388            )
1389            .await
1390            .unwrap();
1391
1392        assert!(result.is_some());
1393        let info = result.unwrap();
1394        assert_eq!(info.resource_id, "user-123");
1395        assert_eq!(info.current_state, "active");
1396    }
1397
1398    #[tokio::test]
1399    async fn test_process_stub_state_with_transition() {
1400        let handler = StatefulResponseHandler::new().unwrap();
1401        let extract = ResourceIdExtract::PathParam {
1402            param: "id".to_string(),
1403        };
1404        let uri: Uri = "/users/user-456".parse().unwrap();
1405        let headers = HeaderMap::new();
1406
1407        // Create initial state
1408        let _ = handler
1409            .process_stub_state(
1410                &Method::GET,
1411                &uri,
1412                &headers,
1413                None,
1414                "user",
1415                &extract,
1416                "active",
1417                None,
1418            )
1419            .await
1420            .unwrap();
1421
1422        // Now process with transition
1423        let transitions = vec![TransitionTrigger {
1424            method: Method::DELETE,
1425            path_pattern: "/users/{id}".to_string(),
1426            from_state: "active".to_string(),
1427            to_state: "deleted".to_string(),
1428            condition: None,
1429        }];
1430
1431        let result = handler
1432            .process_stub_state(
1433                &Method::DELETE,
1434                &uri,
1435                &headers,
1436                None,
1437                "user",
1438                &extract,
1439                "active",
1440                Some(&transitions),
1441            )
1442            .await
1443            .unwrap();
1444
1445        let info = result.unwrap();
1446        assert_eq!(info.current_state, "deleted");
1447    }
1448
1449    // =========================================================================
1450    // Get/Update resource state tests
1451    // =========================================================================
1452
1453    #[tokio::test]
1454    async fn test_get_resource_state_not_found() {
1455        let handler = StatefulResponseHandler::new().unwrap();
1456        let result = handler.get_resource_state("nonexistent", "order").await.unwrap();
1457        assert!(result.is_none());
1458    }
1459
1460    #[tokio::test]
1461    async fn test_get_resource_state_exists() {
1462        let handler = StatefulResponseHandler::new().unwrap();
1463
1464        // Create a resource via stub processing
1465        let extract = ResourceIdExtract::PathParam {
1466            param: "id".to_string(),
1467        };
1468        let uri: Uri = "/orders/order-999".parse().unwrap();
1469        let headers = HeaderMap::new();
1470
1471        handler
1472            .process_stub_state(
1473                &Method::GET,
1474                &uri,
1475                &headers,
1476                None,
1477                "order",
1478                &extract,
1479                "pending",
1480                None,
1481            )
1482            .await
1483            .unwrap();
1484
1485        // Now get the state
1486        let result = handler.get_resource_state("order-999", "order").await.unwrap();
1487        assert!(result.is_some());
1488        assert_eq!(result.unwrap().current_state, "pending");
1489    }
1490
1491    #[tokio::test]
1492    async fn test_update_resource_state() {
1493        let handler = StatefulResponseHandler::new().unwrap();
1494
1495        // Create a resource via stub processing
1496        let extract = ResourceIdExtract::PathParam {
1497            param: "id".to_string(),
1498        };
1499        let uri: Uri = "/orders/order-update".parse().unwrap();
1500        let headers = HeaderMap::new();
1501
1502        handler
1503            .process_stub_state(
1504                &Method::GET,
1505                &uri,
1506                &headers,
1507                None,
1508                "order",
1509                &extract,
1510                "pending",
1511                None,
1512            )
1513            .await
1514            .unwrap();
1515
1516        // Update the state
1517        handler.update_resource_state("order-update", "order", "shipped").await.unwrap();
1518
1519        // Verify update
1520        let result = handler.get_resource_state("order-update", "order").await.unwrap();
1521        assert_eq!(result.unwrap().current_state, "shipped");
1522    }
1523
1524    #[tokio::test]
1525    async fn test_update_resource_state_not_found() {
1526        let handler = StatefulResponseHandler::new().unwrap();
1527        let result = handler.update_resource_state("nonexistent", "order", "shipped").await;
1528        assert!(result.is_err());
1529    }
1530
1531    #[tokio::test]
1532    async fn test_update_resource_state_wrong_type() {
1533        let handler = StatefulResponseHandler::new().unwrap();
1534
1535        // Create a resource with type "order"
1536        let extract = ResourceIdExtract::PathParam {
1537            param: "id".to_string(),
1538        };
1539        let uri: Uri = "/orders/order-type-test".parse().unwrap();
1540        let headers = HeaderMap::new();
1541
1542        handler
1543            .process_stub_state(
1544                &Method::GET,
1545                &uri,
1546                &headers,
1547                None,
1548                "order",
1549                &extract,
1550                "pending",
1551                None,
1552            )
1553            .await
1554            .unwrap();
1555
1556        // Try to update with wrong type
1557        let result = handler.update_resource_state("order-type-test", "user", "active").await;
1558        assert!(result.is_err());
1559    }
1560
1561    // =========================================================================
1562    // Condition evaluation tests
1563    // =========================================================================
1564
1565    #[test]
1566    fn test_evaluate_condition_json_path_truthy() {
1567        let handler = StatefulResponseHandler::new().unwrap();
1568        let headers = HeaderMap::new();
1569        let body = br#"{"verified": "true"}"#;
1570        let result = handler.evaluate_condition("$.verified", &headers, Some(body)).unwrap();
1571        assert!(result);
1572    }
1573
1574    #[test]
1575    fn test_evaluate_condition_json_path_falsy() {
1576        let handler = StatefulResponseHandler::new().unwrap();
1577        let headers = HeaderMap::new();
1578        let body = br#"{"verified": "false"}"#;
1579        let result = handler.evaluate_condition("$.verified", &headers, Some(body)).unwrap();
1580        assert!(!result);
1581    }
1582
1583    #[test]
1584    fn test_evaluate_condition_non_jsonpath() {
1585        let handler = StatefulResponseHandler::new().unwrap();
1586        let headers = HeaderMap::new();
1587        let result = handler.evaluate_condition("some_condition", &headers, None).unwrap();
1588        assert!(result); // Non-JSONPath conditions default to true
1589    }
1590
1591    // =========================================================================
1592    // StateInfo tests
1593    // =========================================================================
1594
1595    #[test]
1596    fn test_state_info_clone() {
1597        let info = StateInfo {
1598            resource_id: "res-1".to_string(),
1599            current_state: "active".to_string(),
1600            state_data: HashMap::new(),
1601        };
1602        let cloned = info.clone();
1603        assert_eq!(cloned.resource_id, "res-1");
1604    }
1605
1606    #[test]
1607    fn test_state_info_debug() {
1608        let info = StateInfo {
1609            resource_id: "res-2".to_string(),
1610            current_state: "inactive".to_string(),
1611            state_data: HashMap::new(),
1612        };
1613        let debug_str = format!("{:?}", info);
1614        assert!(debug_str.contains("res-2"));
1615        assert!(debug_str.contains("inactive"));
1616    }
1617
1618    // =========================================================================
1619    // StatefulResponse tests
1620    // =========================================================================
1621
1622    #[test]
1623    fn test_stateful_response_clone() {
1624        let response = StatefulResponse {
1625            status_code: 200,
1626            headers: HashMap::new(),
1627            body: "{}".to_string(),
1628            content_type: "application/json".to_string(),
1629            state: "active".to_string(),
1630            resource_id: "res-3".to_string(),
1631        };
1632        let cloned = response.clone();
1633        assert_eq!(cloned.status_code, 200);
1634        assert_eq!(cloned.state, "active");
1635    }
1636
1637    #[test]
1638    fn test_stateful_response_debug() {
1639        let response = StatefulResponse {
1640            status_code: 404,
1641            headers: HashMap::new(),
1642            body: "Not found".to_string(),
1643            content_type: "text/plain".to_string(),
1644            state: "deleted".to_string(),
1645            resource_id: "res-4".to_string(),
1646        };
1647        let debug_str = format!("{:?}", response);
1648        assert!(debug_str.contains("404"));
1649        assert!(debug_str.contains("deleted"));
1650    }
1651}