Skip to main content

alien_core/
stack_state.rs

1//!
2//! Defines structures for managing the runtime state of deployed resources.
3//! This includes platform-specific internal states, overall stack status,
4//! resource outputs, error tracking, and pending user actions.
5
6use crate::{
7    Platform, Resource, ResourceLifecycle, ResourceOutputs, ResourceOutputsDefinition, ResourceRef,
8    ResourceStatus,
9};
10
11use alien_error::AlienError;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use std::fmt::Debug;
15use uuid::Uuid;
16
17use crate::{ErrorData, Result};
18
19/// Represents the overall status of a stack based on its resource states.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
22#[serde(rename_all = "snake_case")]
23pub enum StackStatus {
24    /// Stack is initializing with no resources yet created
25    Pending,
26    /// Stack has resources that are currently being provisioned, updated, or deleted
27    InProgress,
28    /// All resources are successfully running and the stack is operational
29    Running,
30    /// All resources have been successfully deleted and the stack is removed
31    Deleted,
32    /// One or more resources have failed during provisioning, updating, or deleting
33    Failure,
34}
35
36/// Represents the collective state of all resources in a stack, including platform and pending actions.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
39#[serde(rename_all = "camelCase")]
40pub struct StackState {
41    /// The target platform for this stack state.
42    pub platform: Platform,
43    /// The state of individual resources, keyed by resource ID.
44    pub resources: HashMap<String, StackResourceState>,
45    /// A prefix used for resource naming to ensure uniqueness across deployments.
46    pub resource_prefix: String,
47}
48
49impl StackState {
50    /// Creates a new, empty StackState for a given platform with a generated resource prefix.
51    pub fn new(platform: Platform) -> Self {
52        // Generate a resource prefix that matches [a-zA-Z][a-zA-Z\d\-]*[a-zA-Z\d] pattern
53        // (e.g., "k44e9b72", "m8a3f1d5")
54        let letters = "abcdefghijklmnopqrstuvwxyz";
55        let first_char = letters
56            .chars()
57            .nth(Uuid::new_v4().as_bytes()[0] as usize % 26)
58            .unwrap();
59        let uuid_part = Uuid::new_v4().simple().to_string()[..7].to_string();
60        let prefix = format!("{}{}", first_char, uuid_part);
61
62        StackState {
63            platform,
64            resources: HashMap::new(),
65            resource_prefix: prefix,
66        }
67    }
68
69    /// Returns a reference to the state of a specific resource if it exists.
70    pub fn resource(&self, id: &str) -> Option<&StackResourceState> {
71        self.resources.get(id)
72    }
73
74    /// Computes the stack status from the current resource statuses.
75    /// This is the main function that implements the logic from the TypeScript version.
76    pub fn compute_stack_status(&self) -> Result<StackStatus> {
77        let resource_statuses: Vec<ResourceStatus> = self
78            .resources
79            .values()
80            .map(|resource| resource.status)
81            .collect();
82
83        Self::compute_stack_status_from_resources(&resource_statuses)
84    }
85
86    /// Static method to compute stack status from a list of resource statuses.
87    /// This method contains the core logic and can be tested independently.
88    pub fn compute_stack_status_from_resources(
89        resource_statuses: &[ResourceStatus],
90    ) -> Result<StackStatus> {
91        // If there are no resources, it's pending (initializing a completely new stack state)
92        if resource_statuses.is_empty() {
93            return Ok(StackStatus::Pending);
94        }
95
96        // Check for any failure states
97        if resource_statuses.iter().any(|status| {
98            matches!(
99                status,
100                ResourceStatus::ProvisionFailed
101                    | ResourceStatus::UpdateFailed
102                    | ResourceStatus::DeleteFailed
103                    | ResourceStatus::RefreshFailed
104            )
105        }) {
106            return Ok(StackStatus::Failure);
107        }
108
109        // Check for any in-progress states
110        if resource_statuses.iter().any(|status| {
111            matches!(
112                status,
113                ResourceStatus::Pending
114                    | ResourceStatus::Provisioning
115                    | ResourceStatus::Updating
116                    | ResourceStatus::Deleting
117            )
118        }) {
119            return Ok(StackStatus::InProgress);
120        }
121
122        // Check for terminal states
123        if resource_statuses
124            .iter()
125            .all(|status| matches!(status, ResourceStatus::Running))
126        {
127            return Ok(StackStatus::Running);
128        }
129
130        if resource_statuses
131            .iter()
132            .all(|status| matches!(status, ResourceStatus::Deleted))
133        {
134            return Ok(StackStatus::Deleted);
135        }
136
137        // Check for mixed Running + Deleted (deletion in progress)
138        // This happens during dependency-ordered deletion when some resources are deleted
139        // but others are still running while waiting for dependencies to clear
140        let has_running = resource_statuses
141            .iter()
142            .any(|status| matches!(status, ResourceStatus::Running));
143        let has_deleted = resource_statuses
144            .iter()
145            .any(|status| matches!(status, ResourceStatus::Deleted));
146        let only_running_or_deleted = resource_statuses
147            .iter()
148            .all(|status| matches!(status, ResourceStatus::Running | ResourceStatus::Deleted));
149
150        if has_running && has_deleted && only_running_or_deleted {
151            return Ok(StackStatus::InProgress);
152        }
153
154        // Mixed terminal states or unexpected combinations
155        let status_strings: Vec<String> = resource_statuses
156            .iter()
157            .map(|status| format!("{:?}", status).to_lowercase().replace('_', "-"))
158            .collect();
159
160        Err(AlienError::new(
161            ErrorData::UnexpectedResourceStatusCombination {
162                resource_statuses: status_strings,
163                operation: "stack status computation".to_string(),
164            },
165        ))
166    }
167
168    /// Retrieves and downcasts the outputs of a resource from the stack state.
169    ///
170    /// # Arguments
171    /// * `resource_id` - The ID of the resource to get outputs for
172    ///
173    /// # Returns
174    /// * `Ok(T)` - The downcasted outputs if successful
175    /// * `Err(Error)` - If the resource doesn't exist, has no outputs, or the outputs are not of the expected type
176    ///
177    /// # Example
178    /// ```rust,ignore
179    /// use alien_core::{StackState, Platform, FunctionOutputs};
180    ///
181    /// let stack_state = StackState::new(Platform::Aws);
182    ///
183    /// // Get function outputs with error handling
184    /// let function_outputs = stack_state.get_resource_outputs::<FunctionOutputs>("my-function")?;
185    /// if let Some(url) = &function_outputs.url {
186    ///     println!("Function URL: {}", url);
187    /// }
188    /// ```
189    pub fn get_resource_outputs<T: ResourceOutputsDefinition + 'static>(
190        &self,
191        resource_id: &str,
192    ) -> Result<&T> {
193        let resource_state = self.resources.get(resource_id).ok_or_else(|| {
194            AlienError::new(ErrorData::ResourceNotFound {
195                resource_id: resource_id.to_string(),
196                available_resources: self.resources.keys().cloned().collect(),
197            })
198        })?;
199
200        let outputs = resource_state.outputs.as_ref().ok_or_else(|| {
201            AlienError::new(ErrorData::ResourceHasNoOutputs {
202                resource_id: resource_id.to_string(),
203            })
204        })?;
205
206        outputs.downcast_ref::<T>().ok_or_else(|| {
207            AlienError::new(ErrorData::UnexpectedResourceType {
208                resource_id: resource_id.to_string(),
209                expected: T::resource_type(),
210                actual: resource_state.resource_type.clone().into(),
211            })
212        })
213    }
214}
215
216/// Represents the state of a single resource within the stack for a specific platform.
217#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
218#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
219#[serde(rename_all = "camelCase")]
220pub struct StackResourceState {
221    /// The high-level type of the resource (e.g., Function::RESOURCE_TYPE, Storage::RESOURCE_TYPE).
222    #[serde(rename = "type")]
223    pub resource_type: String,
224
225    /// The platform-specific resource controller that manages this resource's lifecycle.
226    /// This is None when the resource status is Pending.
227    /// Stored as JSON to make the struct serializable and movable to alien-core.
228    #[serde(rename = "_internal", skip_serializing_if = "Option::is_none")]
229    pub internal_state: Option<serde_json::Value>,
230
231    /// High-level status derived from the internal state.
232    pub status: ResourceStatus,
233
234    /// Outputs generated by the resource (e.g., ARN, URL, Bucket Name).
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub outputs: Option<ResourceOutputs>,
237
238    /// The current resource configuration.
239    pub config: Resource,
240
241    /// The previous resource configuration during updates.
242    /// This is set when an update is initiated and cleared when the update completes or fails.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub previous_config: Option<Resource>,
245
246    /// Tracks consecutive retry attempts for the current state transition.
247    #[serde(default, skip_serializing_if = "is_zero")]
248    #[builder(default)]
249    pub retry_attempt: u32,
250
251    /// Stores the last error encountered during a failed step transition.
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub error: Option<AlienError>,
254
255    /// True if the resource was provisioned by an external system (e.g., CloudFormation).
256    /// Defaults to false, indicating dynamic provisioning by the executor.
257    #[serde(default, skip_serializing_if = "is_false")]
258    #[builder(default)]
259    pub is_externally_provisioned: bool,
260
261    /// The lifecycle of the resource (Frozen, Live, LiveOnSetup).
262    /// Defaults to Live if not specified.
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub lifecycle: Option<ResourceLifecycle>,
265
266    /// Complete list of dependencies for this resource, including infrastructure dependencies.
267    /// This preserves the full dependency information from the stack definition.
268    #[serde(default, skip_serializing_if = "Vec::is_empty")]
269    #[builder(default = vec![])]
270    pub dependencies: Vec<ResourceRef>,
271
272    /// Stores the controller state that failed, used for manual retry operations.
273    /// This allows resuming from the exact point where the failure occurred.
274    /// Stored as JSON to make the struct serializable and movable to alien-core.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub last_failed_state: Option<serde_json::Value>,
277
278    /// Binding parameters for remote access.
279    /// Only populated when the resource has `remote_access: true` in its ResourceEntry.
280    /// This is the JSON serialization of the binding configuration (e.g., StorageBinding, VaultBinding).
281    /// Populated by controllers during provisioning using get_binding_params().
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub remote_binding_params: Option<serde_json::Value>,
284}
285
286impl StackResourceState {
287    /// Creates a new pending StackResourceState for a resource that's about to be created
288    pub fn new_pending(
289        resource_type: String,
290        config: Resource,
291        lifecycle: Option<ResourceLifecycle>,
292        dependencies: Vec<ResourceRef>,
293    ) -> Self {
294        Self {
295            resource_type,
296            internal_state: None,
297            status: ResourceStatus::Pending,
298            outputs: None,
299            config,
300            previous_config: None,
301            retry_attempt: 0,
302            error: None,
303            is_externally_provisioned: false,
304            lifecycle,
305            dependencies,
306            last_failed_state: None,
307            remote_binding_params: None,
308        }
309    }
310
311    /// Creates a new StackResourceState based on this one, with only the specified fields modified
312    pub fn with_updates<F>(&self, update_fn: F) -> Self
313    where
314        F: FnOnce(&mut Self),
315    {
316        let mut new_state = self.clone();
317        update_fn(&mut new_state);
318        new_state
319    }
320
321    /// Creates a new StackResourceState with the status changed to a failure state and error set
322    pub fn with_failure(&self, status: ResourceStatus, error: AlienError) -> Self {
323        self.with_updates(|state| {
324            state.status = status;
325            state.error = Some(error);
326            state.retry_attempt = 0;
327        })
328    }
329}
330
331// Helper function for skip_serializing_if on retry_attempt
332fn is_zero(num: &u32) -> bool {
333    *num == 0
334}
335
336// Helper function for skip_serializing_if on is_externally_provisioned
337fn is_false(b: &bool) -> bool {
338    !*b
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344    use crate::{Function, FunctionCode, FunctionOutputs, ResourceType, Storage, StorageOutputs};
345
346    #[test]
347    fn test_get_resource_outputs_success() {
348        let mut stack_state = StackState::new(Platform::Aws);
349
350        // Create a function with outputs
351        let function_outputs = FunctionOutputs {
352            function_name: "test-function".to_string(),
353            url: Some("https://example.lambda-url.us-east-1.on.aws/".to_string()),
354            identifier: Some(
355                "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
356            ),
357            load_balancer_endpoint: None,
358        };
359
360        let test_function = Function::new("test-function".to_string())
361            .code(FunctionCode::Image {
362                image: "test:latest".to_string(),
363            })
364            .permissions("test-profile".to_string())
365            .build();
366
367        let resource_state = StackResourceState::new_pending(
368            "function".to_string(),
369            Resource::new(test_function),
370            None,
371            Vec::new(),
372        )
373        .with_updates(|state| {
374            state.status = ResourceStatus::Running;
375            state.outputs = Some(ResourceOutputs::new(function_outputs.clone()));
376        });
377
378        stack_state
379            .resources
380            .insert("test-function".to_string(), resource_state);
381
382        // Test successful retrieval
383        let retrieved_outputs = stack_state
384            .get_resource_outputs::<FunctionOutputs>("test-function")
385            .unwrap();
386        assert_eq!(retrieved_outputs.function_name, "test-function");
387        assert_eq!(
388            retrieved_outputs.url,
389            Some("https://example.lambda-url.us-east-1.on.aws/".to_string())
390        );
391        assert_eq!(
392            retrieved_outputs.identifier,
393            Some("arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string())
394        );
395    }
396
397    #[test]
398    fn test_get_resource_outputs_resource_not_found() {
399        let stack_state = StackState::new(Platform::Aws);
400
401        // Test resource not found
402        let result = stack_state.get_resource_outputs::<FunctionOutputs>("nonexistent-function");
403        assert!(result.is_err());
404        let error = result.unwrap_err();
405
406        // Assert on the specific error variant
407        let error_data = &error.error;
408        if let Some(ErrorData::ResourceNotFound {
409            resource_id,
410            available_resources,
411        }) = error_data
412        {
413            assert_eq!(resource_id, "nonexistent-function");
414            assert_eq!(available_resources, &Vec::<String>::new());
415        } else {
416            panic!("Expected ResourceNotFound error, got: {:?}", error_data);
417        }
418
419        // Also check the string representation
420        let error_message = error.to_string();
421        assert!(error_message.contains("Resource 'nonexistent-function' not found in stack state"));
422        assert!(error_message.contains("Available resources: []"));
423    }
424
425    #[test]
426    fn test_get_resource_outputs_no_outputs() {
427        let mut stack_state = StackState::new(Platform::Aws);
428
429        // Create a resource without outputs
430        let test_function_2 = Function::new("test-function".to_string())
431            .code(FunctionCode::Image {
432                image: "test:latest".to_string(),
433            })
434            .permissions("test-profile".to_string())
435            .build();
436
437        let resource_state = StackResourceState::new_pending(
438            "function".to_string(),
439            Resource::new(test_function_2),
440            None,
441            Vec::new(),
442        )
443        .with_updates(|state| {
444            state.status = ResourceStatus::Provisioning;
445        });
446
447        stack_state
448            .resources
449            .insert("test-function".to_string(), resource_state);
450
451        // Test no outputs
452        let result = stack_state.get_resource_outputs::<FunctionOutputs>("test-function");
453        assert!(result.is_err());
454        let error = result.unwrap_err();
455
456        // Assert on the specific error variant
457        let error_data = &error.error;
458        if let Some(ErrorData::ResourceHasNoOutputs { resource_id, .. }) = error_data {
459            assert_eq!(resource_id, "test-function");
460        } else {
461            panic!("Expected ResourceHasNoOutputs error, got: {:?}", error_data);
462        }
463
464        // Also check the string representation
465        let error_message = error.to_string();
466        assert!(error_message.contains("Resource 'test-function' has no outputs"));
467    }
468
469    #[test]
470    fn test_get_resource_outputs_wrong_type() {
471        let mut stack_state = StackState::new(Platform::Aws);
472
473        // Create a storage resource with storage outputs
474        let storage_outputs = StorageOutputs {
475            bucket_name: "test-bucket".to_string(),
476        };
477
478        let test_storage = Storage::new("test-storage".to_string()).build();
479
480        let resource_state = StackResourceState::new_pending(
481            "storage".to_string(),
482            Resource::new(test_storage),
483            None,
484            Vec::new(),
485        )
486        .with_updates(|state| {
487            state.status = ResourceStatus::Running;
488            state.outputs = Some(ResourceOutputs::new(storage_outputs));
489        });
490
491        stack_state
492            .resources
493            .insert("test-storage".to_string(), resource_state);
494
495        // Try to get function outputs from a storage resource
496        let result = stack_state.get_resource_outputs::<FunctionOutputs>("test-storage");
497        assert!(result.is_err());
498        let error = result.unwrap_err();
499
500        // Assert on the specific error variant
501        let error_data = &error.error;
502        if let Some(ErrorData::UnexpectedResourceType {
503            resource_id,
504            expected,
505            actual,
506        }) = error_data
507        {
508            assert_eq!(resource_id, "test-storage");
509            assert_eq!(*expected, ResourceType::from_static("function"));
510            assert_eq!(*actual, ResourceType::from_static("storage"));
511        } else {
512            panic!(
513                "Expected UnexpectedResourceType error, got: {:?}",
514                error_data
515            );
516        }
517    }
518
519    #[test]
520    fn test_get_resource_outputs_usage_example() {
521        let mut stack_state = StackState::new(Platform::Aws);
522
523        // Create a function with outputs (similar to your original sketch)
524        let function_outputs = FunctionOutputs {
525            function_name: "test-alien-function".to_string(),
526            url: Some("https://test.lambda-url.us-east-1.on.aws/".to_string()),
527            identifier: Some(
528                "arn:aws:lambda:us-east-1:123456789012:function:test-alien-function".to_string(),
529            ),
530            load_balancer_endpoint: None,
531        };
532
533        let test_alien_function = Function::new("test-alien-function".to_string())
534            .code(FunctionCode::Image {
535                image: "test:latest".to_string(),
536            })
537            .permissions("test-profile".to_string())
538            .build();
539
540        let resource_state = StackResourceState {
541            resource_type: "function".to_string(),
542            internal_state: None,
543            status: ResourceStatus::Running,
544            outputs: Some(ResourceOutputs::new(function_outputs)),
545            config: Resource::new(test_alien_function),
546            previous_config: None,
547            retry_attempt: 0,
548            error: None,
549            is_externally_provisioned: false,
550            lifecycle: None,
551            dependencies: Vec::new(),
552            last_failed_state: None,
553            remote_binding_params: None,
554        };
555
556        stack_state
557            .resources
558            .insert("test-alien-function".to_string(), resource_state);
559
560        // Test the usage pattern from your original sketch
561        let function_outputs = stack_state
562            .get_resource_outputs::<FunctionOutputs>("test-alien-function")
563            .unwrap();
564
565        let function_url = function_outputs
566            .url
567            .as_ref()
568            .ok_or_else(|| "Function URL not found in stack state")
569            .unwrap();
570
571        assert_eq!(function_url, "https://test.lambda-url.us-east-1.on.aws/");
572    }
573
574    // Tests for StackStatus computation - ported from TypeScript
575    #[cfg(test)]
576    mod stack_status_tests {
577        use super::*;
578
579        #[test]
580        fn test_compute_stack_status_empty_resources() {
581            let result = StackState::compute_stack_status_from_resources(&[]).unwrap();
582            assert_eq!(result, StackStatus::Pending);
583        }
584
585        #[test]
586        fn test_compute_stack_status_single_pending() {
587            let result =
588                StackState::compute_stack_status_from_resources(&[ResourceStatus::Pending])
589                    .unwrap();
590            assert_eq!(result, StackStatus::InProgress);
591        }
592
593        #[test]
594        fn test_compute_stack_status_single_provisioning() {
595            let result =
596                StackState::compute_stack_status_from_resources(&[ResourceStatus::Provisioning])
597                    .unwrap();
598            assert_eq!(result, StackStatus::InProgress);
599        }
600
601        #[test]
602        fn test_compute_stack_status_single_updating() {
603            let result =
604                StackState::compute_stack_status_from_resources(&[ResourceStatus::Updating])
605                    .unwrap();
606            assert_eq!(result, StackStatus::InProgress);
607        }
608
609        #[test]
610        fn test_compute_stack_status_single_deleting() {
611            let result =
612                StackState::compute_stack_status_from_resources(&[ResourceStatus::Deleting])
613                    .unwrap();
614            assert_eq!(result, StackStatus::InProgress);
615        }
616
617        #[test]
618        fn test_compute_stack_status_single_provision_failed() {
619            let result =
620                StackState::compute_stack_status_from_resources(&[ResourceStatus::ProvisionFailed])
621                    .unwrap();
622            assert_eq!(result, StackStatus::Failure);
623        }
624
625        #[test]
626        fn test_compute_stack_status_single_update_failed() {
627            let result =
628                StackState::compute_stack_status_from_resources(&[ResourceStatus::UpdateFailed])
629                    .unwrap();
630            assert_eq!(result, StackStatus::Failure);
631        }
632
633        #[test]
634        fn test_compute_stack_status_single_delete_failed() {
635            let result =
636                StackState::compute_stack_status_from_resources(&[ResourceStatus::DeleteFailed])
637                    .unwrap();
638            assert_eq!(result, StackStatus::Failure);
639        }
640
641        #[test]
642        fn test_compute_stack_status_single_refresh_failed() {
643            let result =
644                StackState::compute_stack_status_from_resources(&[ResourceStatus::RefreshFailed])
645                    .unwrap();
646            assert_eq!(result, StackStatus::Failure);
647        }
648
649        #[test]
650        fn test_compute_stack_status_single_running() {
651            let result =
652                StackState::compute_stack_status_from_resources(&[ResourceStatus::Running])
653                    .unwrap();
654            assert_eq!(result, StackStatus::Running);
655        }
656
657        #[test]
658        fn test_compute_stack_status_single_deleted() {
659            let result =
660                StackState::compute_stack_status_from_resources(&[ResourceStatus::Deleted])
661                    .unwrap();
662            assert_eq!(result, StackStatus::Deleted);
663        }
664
665        #[test]
666        fn test_compute_stack_status_all_running() {
667            let statuses = vec![
668                ResourceStatus::Running,
669                ResourceStatus::Running,
670                ResourceStatus::Running,
671            ];
672            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
673            assert_eq!(result, StackStatus::Running);
674        }
675
676        #[test]
677        fn test_compute_stack_status_all_deleted() {
678            let statuses = vec![
679                ResourceStatus::Deleted,
680                ResourceStatus::Deleted,
681                ResourceStatus::Deleted,
682            ];
683            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
684            assert_eq!(result, StackStatus::Deleted);
685        }
686
687        #[test]
688        fn test_compute_stack_status_all_pending() {
689            let statuses = vec![
690                ResourceStatus::Pending,
691                ResourceStatus::Pending,
692                ResourceStatus::Pending,
693            ];
694            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
695            assert_eq!(result, StackStatus::InProgress);
696        }
697
698        #[test]
699        fn test_compute_stack_status_all_provisioning() {
700            let statuses = vec![
701                ResourceStatus::Provisioning,
702                ResourceStatus::Provisioning,
703                ResourceStatus::Provisioning,
704            ];
705            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
706            assert_eq!(result, StackStatus::InProgress);
707        }
708
709        #[test]
710        fn test_compute_stack_status_all_provision_failed() {
711            let statuses = vec![
712                ResourceStatus::ProvisionFailed,
713                ResourceStatus::ProvisionFailed,
714                ResourceStatus::ProvisionFailed,
715            ];
716            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
717            assert_eq!(result, StackStatus::Failure);
718        }
719
720        #[test]
721        fn test_compute_stack_status_mixed_with_failure() {
722            let statuses = vec![
723                ResourceStatus::Running,
724                ResourceStatus::ProvisionFailed,
725                ResourceStatus::Updating,
726            ];
727            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
728            assert_eq!(result, StackStatus::Failure);
729        }
730
731        #[test]
732        fn test_compute_stack_status_failure_with_success() {
733            let statuses = vec![
734                ResourceStatus::Running,
735                ResourceStatus::UpdateFailed,
736                ResourceStatus::Running,
737            ];
738            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
739            assert_eq!(result, StackStatus::Failure);
740        }
741
742        #[test]
743        fn test_compute_stack_status_failure_with_in_progress() {
744            let statuses = vec![
745                ResourceStatus::Provisioning,
746                ResourceStatus::DeleteFailed,
747                ResourceStatus::Deleting,
748            ];
749            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
750            assert_eq!(result, StackStatus::Failure);
751        }
752
753        #[test]
754        fn test_compute_stack_status_any_in_progress() {
755            let statuses = vec![
756                ResourceStatus::Running,
757                ResourceStatus::Updating,
758                ResourceStatus::Running,
759            ];
760            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
761            assert_eq!(result, StackStatus::InProgress);
762        }
763
764        #[test]
765        fn test_compute_stack_status_mixed_in_progress_states() {
766            let statuses = vec![
767                ResourceStatus::Pending,
768                ResourceStatus::Provisioning,
769                ResourceStatus::Updating,
770                ResourceStatus::Deleting,
771            ];
772            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
773            assert_eq!(result, StackStatus::InProgress);
774        }
775
776        #[test]
777        fn test_compute_stack_status_deletion_in_progress() {
778            // During deletion, some resources are deleted while others are still running
779            // (waiting for dependencies to clear). This should be InProgress, not an error.
780            let statuses = vec![ResourceStatus::Running, ResourceStatus::Deleted];
781            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
782            assert_eq!(result, StackStatus::InProgress);
783        }
784
785        #[test]
786        fn test_compute_stack_status_deletion_in_progress_many_resources() {
787            // Test with a more realistic scenario: 9 resources, 2 deleted, 7 still running
788            let statuses = vec![
789                ResourceStatus::Running,
790                ResourceStatus::Running,
791                ResourceStatus::Deleted,
792                ResourceStatus::Deleted,
793                ResourceStatus::Running,
794                ResourceStatus::Running,
795                ResourceStatus::Running,
796                ResourceStatus::Running,
797                ResourceStatus::Running,
798            ];
799            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
800            assert_eq!(result, StackStatus::InProgress);
801        }
802
803        #[test]
804        fn test_compute_stack_status_mixed_terminal_with_in_progress() {
805            let statuses = vec![
806                ResourceStatus::Running,
807                ResourceStatus::Deleted,
808                ResourceStatus::Pending,
809            ];
810            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
811            assert_eq!(result, StackStatus::InProgress);
812        }
813
814        #[test]
815        fn test_compute_stack_status_large_number_of_resources() {
816            let statuses: Vec<ResourceStatus> = (0..100).map(|_| ResourceStatus::Running).collect();
817            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
818            assert_eq!(result, StackStatus::Running);
819        }
820
821        #[test]
822        fn test_compute_stack_status_single_failure_among_many() {
823            let mut statuses: Vec<ResourceStatus> =
824                (0..50).map(|_| ResourceStatus::Running).collect();
825            statuses.push(ResourceStatus::ProvisionFailed);
826            statuses.extend((0..49).map(|_| ResourceStatus::Provisioning));
827
828            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
829            assert_eq!(result, StackStatus::Failure);
830        }
831
832        #[test]
833        fn test_compute_stack_status_failure_priority_over_in_progress() {
834            let statuses = vec![
835                ResourceStatus::ProvisionFailed,
836                ResourceStatus::UpdateFailed,
837                ResourceStatus::DeleteFailed,
838                ResourceStatus::Provisioning,
839                ResourceStatus::Updating,
840                ResourceStatus::Deleting,
841            ];
842            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
843            assert_eq!(result, StackStatus::Failure);
844        }
845
846        #[test]
847        fn test_compute_stack_status_mixed_success_and_in_progress() {
848            let statuses = vec![
849                ResourceStatus::Running,
850                ResourceStatus::Provisioning,
851                ResourceStatus::Running,
852            ];
853            let result = StackState::compute_stack_status_from_resources(&statuses).unwrap();
854            assert_eq!(result, StackStatus::InProgress);
855        }
856
857        #[test]
858        fn test_stack_state_status_computation() {
859            let mut stack_state = StackState::new(Platform::Aws);
860
861            // Initially should be pending (no resources)
862            assert_eq!(
863                stack_state.compute_stack_status().unwrap(),
864                StackStatus::Pending
865            );
866
867            // Add a running resource
868            let test_function = Function::new("test-function".to_string())
869                .code(FunctionCode::Image {
870                    image: "test:latest".to_string(),
871                })
872                .permissions("test-profile".to_string())
873                .build();
874
875            let resource_state = StackResourceState::new_pending(
876                "function".to_string(),
877                Resource::new(test_function),
878                None,
879                Vec::new(),
880            )
881            .with_updates(|state| {
882                state.status = ResourceStatus::Running;
883            });
884
885            stack_state
886                .resources
887                .insert("test-function".to_string(), resource_state);
888
889            // Compute status
890            assert_eq!(
891                stack_state.compute_stack_status().unwrap(),
892                StackStatus::Running
893            );
894        }
895    }
896}