Skip to main content

torrust_tracker_deployer_lib/domain/environment/state/
mod.rs

1//! Environment State Marker Types
2//!
3//! This module defines the state marker types used in the type-state pattern
4//! for the Environment entity. Each state represents a distinct phase in the
5//! deployment lifecycle and is enforced at compile-time.
6//!
7//! ## Type-State Pattern
8//!
9//! The type-state pattern uses Rust's type system to enforce state machine
10//! transitions at compile-time. Each state is a distinct type, and the
11//! `Environment<S>` struct is generic over the state type. This ensures that
12//! invalid state transitions are caught during compilation rather than at runtime.
13//!
14//! ## State Lifecycle
15//!
16//! ### Happy Path
17//!
18//! ```text
19//! Created → Provisioning → Provisioned → Configuring → Configured
20//!   → Releasing → Released → Running → Destroyed
21//! ```
22//!
23//! ### Error States
24//!
25//! At each operational phase, the system can transition to a corresponding
26//! failed state if an error occurs:
27//!
28//! - `Provisioning` → `ProvisionFailed`
29//! - `Configuring` → `ConfigureFailed`
30//! - `Releasing` → `ReleaseFailed`
31//! - `Running` → `RunFailed`
32//!
33//! ## Usage Example
34//!
35//! ```rust
36//! use torrust_tracker_deployer_lib::domain::environment::state::{Created, Provisioning};
37//!
38//! // State types are used as type parameters for Environment<S>
39//! // let env: Environment<Created> = Environment::new(name, credentials);
40//! // let env: Environment<Provisioning> = env.start_provisioning();
41//! ```
42
43use serde::{Deserialize, Serialize};
44use thiserror::Error;
45
46use crate::domain::environment::runtime_outputs::{ProvisionMethod, ServiceEndpoints};
47use crate::shared::domain_name::DomainName;
48
49// State modules
50mod common;
51mod configure_failed;
52mod configured;
53mod configuring;
54mod created;
55mod destroy_failed;
56mod destroyed;
57mod destroying;
58mod provision_failed;
59mod provisioned;
60mod provisioning;
61mod release_failed;
62mod released;
63mod releasing;
64mod run_failed;
65mod running;
66
67// Re-export state types
68pub use common::BaseFailureContext;
69pub use configure_failed::{ConfigureFailed, ConfigureFailureContext, ConfigureStep};
70pub use configured::Configured;
71pub use configuring::Configuring;
72pub use created::Created;
73pub use destroy_failed::{DestroyFailed, DestroyFailureContext, DestroyStep};
74pub use destroyed::Destroyed;
75pub use destroying::Destroying;
76pub use provision_failed::{ProvisionFailed, ProvisionFailureContext, ProvisionStep};
77pub use provisioned::Provisioned;
78pub use provisioning::Provisioning;
79pub use release_failed::{ReleaseFailed, ReleaseFailureContext, ReleaseStep};
80pub use released::Released;
81pub use releasing::Releasing;
82pub use run_failed::{RunFailed, RunFailureContext, RunStep};
83pub use running::Running;
84
85/// Error type for invalid type conversions when working with type-erased environments
86///
87/// This error occurs when attempting to convert an `AnyEnvironmentState` to a specific
88/// typed `Environment<S>` state, but the runtime state doesn't match the expected type.
89///
90/// # Example
91///
92/// ```rust
93/// use torrust_tracker_deployer_lib::domain::environment::state::AnyEnvironmentState;
94///
95/// // let any_env = AnyEnvironmentState::Provisioned(...);
96/// // // This will fail because any_env is Provisioned, not Created
97/// // let result = any_env.try_into_created();
98/// // assert!(result.is_err());
99/// ```
100#[derive(Debug, Clone, Error)]
101pub enum StateTypeError {
102    /// The environment is in a different state than expected
103    #[error("Expected state '{expected}', but found '{actual}'")]
104    UnexpectedState {
105        /// The state type that was expected
106        expected: &'static str,
107        /// The actual state type that was found
108        actual: String,
109    },
110}
111
112// Import Environment for type erasure enum
113use crate::domain::environment::{Environment, EnvironmentName};
114
115/// Type-erased environment that can hold any typed `Environment<S>` at runtime
116///
117/// This enum enables runtime handling of `Environment<S>` instances without
118/// knowing their specific state type at compile time. This is essential for:
119///
120/// - **Serialization**: Saving environments to disk (JSON files)
121/// - **Deserialization**: Loading environments from disk
122/// - **Collections**: Storing environments with different states together
123/// - **Runtime Inspection**: Checking state without compile-time type knowledge
124/// - **Generic Interfaces**: Passing through non-generic function parameters
125///
126/// ## Type Erasure Pattern
127///
128/// Each variant wraps a typed `Environment<S>` where `S` is one of the state
129/// marker types defined in this module. The enum variant name acts as a
130/// discriminator (similar to a `type` column in database Single Table Inheritance).
131///
132/// ## Usage Example
133///
134/// ```rust
135/// use torrust_tracker_deployer_lib::domain::environment::state::AnyEnvironmentState;
136///
137/// // Type erasure: typed -> runtime
138/// // let env: Environment<Provisioned> = ...;
139/// // let any_env: AnyEnvironmentState = env.into_any();
140///
141/// // Serialization
142/// // let json = serde_json::to_string(&any_env)?;
143///
144/// // Deserialization
145/// // let any_env: AnyEnvironmentState = serde_json::from_str(&json)?;
146///
147/// // Type restoration: runtime -> typed
148/// // let env: Environment<Provisioned> = any_env.try_into_provisioned()?;
149/// ```
150///
151/// ## Design Decision
152///
153/// See [ADR: Type Erasure for Environment States](../../docs/decisions/type-erasure-for-environment-states.md)
154/// for detailed rationale behind this design choice.
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub enum AnyEnvironmentState {
157    /// Environment in `Created` state
158    Created(Environment<Created>),
159
160    /// Environment in `Provisioning` state
161    Provisioning(Environment<Provisioning>),
162
163    /// Environment in `Provisioned` state
164    Provisioned(Environment<Provisioned>),
165
166    /// Environment in `Configuring` state
167    Configuring(Environment<Configuring>),
168
169    /// Environment in `Configured` state
170    Configured(Environment<Configured>),
171
172    /// Environment in `Releasing` state
173    Releasing(Environment<Releasing>),
174
175    /// Environment in `Released` state
176    Released(Environment<Released>),
177
178    /// Environment in `Running` state
179    Running(Environment<Running>),
180
181    /// Environment in `Destroying` state
182    Destroying(Environment<Destroying>),
183
184    /// Environment in `ProvisionFailed` error state
185    ProvisionFailed(Environment<ProvisionFailed>),
186
187    /// Environment in `ConfigureFailed` error state
188    ConfigureFailed(Environment<ConfigureFailed>),
189
190    /// Environment in `ReleaseFailed` error state
191    ReleaseFailed(Environment<ReleaseFailed>),
192
193    /// Environment in `RunFailed` error state
194    RunFailed(Environment<RunFailed>),
195
196    /// Environment in `DestroyFailed` error state
197    DestroyFailed(Environment<DestroyFailed>),
198
199    /// Environment in `Destroyed` terminal state
200    Destroyed(Environment<Destroyed>),
201}
202
203// Introspection methods for AnyEnvironmentState
204impl AnyEnvironmentState {
205    /// Get a reference to the environment context regardless of current state
206    ///
207    /// This helper method centralizes state matching for accessing
208    /// state-independent data. Instead of pattern matching 6 times
209    /// (once per field accessor), we match once and reuse the result.
210    ///
211    /// This is a private implementation detail that simplifies the
212    /// public accessor methods.
213    ///
214    /// # Returns
215    ///
216    /// A reference to the `EnvironmentContext` contained within the environment.
217    fn context(&self) -> &crate::domain::environment::EnvironmentContext {
218        match self {
219            Self::Created(env) => env.context(),
220            Self::Provisioning(env) => env.context(),
221            Self::Provisioned(env) => env.context(),
222            Self::Configuring(env) => env.context(),
223            Self::Configured(env) => env.context(),
224            Self::Releasing(env) => env.context(),
225            Self::Released(env) => env.context(),
226            Self::Running(env) => env.context(),
227            Self::Destroying(env) => env.context(),
228            Self::ProvisionFailed(env) => env.context(),
229            Self::ConfigureFailed(env) => env.context(),
230            Self::ReleaseFailed(env) => env.context(),
231            Self::RunFailed(env) => env.context(),
232            Self::DestroyFailed(env) => env.context(),
233            Self::Destroyed(env) => env.context(),
234        }
235    }
236
237    /// Get the environment name regardless of current state
238    ///
239    /// This method provides access to the environment name without needing to
240    /// pattern match on the specific state variant.
241    ///
242    /// # Returns
243    ///
244    /// A reference to the `EnvironmentName` contained within the environment.
245    #[must_use]
246    pub fn name(&self) -> &EnvironmentName {
247        self.context().user_inputs.name()
248    }
249
250    /// Get the state name as a string
251    ///
252    /// Returns a static string identifier for the current state. This is useful
253    /// for logging, error messages, and displaying state information to users.
254    ///
255    /// # Returns
256    ///
257    /// A static string representing the state name (e.g., "created", "provisioning").
258    #[must_use]
259    pub fn state_name(&self) -> &'static str {
260        match self {
261            Self::Created(_) => "created",
262            Self::Provisioning(_) => "provisioning",
263            Self::Provisioned(_) => "provisioned",
264            Self::Configuring(_) => "configuring",
265            Self::Configured(_) => "configured",
266            Self::Releasing(_) => "releasing",
267            Self::Released(_) => "released",
268            Self::Running(_) => "running",
269            Self::Destroying(_) => "destroying",
270            Self::ProvisionFailed(_) => "provision_failed",
271            Self::ConfigureFailed(_) => "configure_failed",
272            Self::ReleaseFailed(_) => "release_failed",
273            Self::RunFailed(_) => "run_failed",
274            Self::DestroyFailed(_) => "destroy_failed",
275            Self::Destroyed(_) => "destroyed",
276        }
277    }
278
279    /// Returns a human-readable display name for the current state.
280    ///
281    /// This provides a user-friendly representation suitable for CLI output,
282    /// reports, and other user-facing contexts. Failed states include a space
283    /// for readability (e.g., "Provision Failed").
284    ///
285    /// # Returns
286    ///
287    /// A static string representing the display name (e.g., "Created", "Provision Failed").
288    #[must_use]
289    pub fn state_display_name(&self) -> &'static str {
290        match self {
291            Self::Created(_) => "Created",
292            Self::Provisioning(_) => "Provisioning",
293            Self::Provisioned(_) => "Provisioned",
294            Self::Configuring(_) => "Configuring",
295            Self::Configured(_) => "Configured",
296            Self::Releasing(_) => "Releasing",
297            Self::Released(_) => "Released",
298            Self::Running(_) => "Running",
299            Self::Destroying(_) => "Destroying",
300            Self::ProvisionFailed(_) => "Provision Failed",
301            Self::ConfigureFailed(_) => "Configure Failed",
302            Self::ReleaseFailed(_) => "Release Failed",
303            Self::RunFailed(_) => "Run Failed",
304            Self::DestroyFailed(_) => "Destroy Failed",
305            Self::Destroyed(_) => "Destroyed",
306        }
307    }
308
309    /// Check if the environment is in a success (non-error) state
310    ///
311    /// Success states are those representing normal operation flow, including
312    /// transient states (like `Provisioning`) and terminal success states
313    /// (like `Running`, `Destroyed`).
314    ///
315    /// # Returns
316    ///
317    /// `true` if the environment is in a success state, `false` for error states.
318    #[must_use]
319    pub fn is_success_state(&self) -> bool {
320        matches!(
321            self,
322            Self::Created(_)
323                | Self::Provisioning(_)
324                | Self::Provisioned(_)
325                | Self::Configuring(_)
326                | Self::Configured(_)
327                | Self::Releasing(_)
328                | Self::Released(_)
329                | Self::Running(_)
330                | Self::Destroying(_)
331                | Self::Destroyed(_)
332        )
333    }
334
335    /// Check if the environment is in an error state
336    ///
337    /// Error states indicate that an operation failed during the environment's
338    /// lifecycle (provisioning, configuration, release, or runtime).
339    ///
340    /// # Returns
341    ///
342    /// `true` if the environment is in an error state, `false` otherwise.
343    #[must_use]
344    pub fn is_error_state(&self) -> bool {
345        matches!(
346            self,
347            Self::ProvisionFailed(_)
348                | Self::ConfigureFailed(_)
349                | Self::ReleaseFailed(_)
350                | Self::RunFailed(_)
351                | Self::DestroyFailed(_)
352        )
353    }
354
355    /// Check if the environment is in a terminal state
356    ///
357    /// Terminal states are final states where no more transitions are expected.
358    /// This includes both successful terminal states (`Running`, `Destroyed`)
359    /// and error states (all `*Failed` variants).
360    ///
361    /// # Returns
362    ///
363    /// `true` if the environment is in a terminal state, `false` otherwise.
364    #[must_use]
365    pub fn is_terminal_state(&self) -> bool {
366        matches!(
367            self,
368            Self::Running(_)
369                | Self::Destroyed(_)
370                | Self::ProvisionFailed(_)
371                | Self::ConfigureFailed(_)
372                | Self::ReleaseFailed(_)
373                | Self::RunFailed(_)
374                | Self::DestroyFailed(_)
375        )
376    }
377
378    /// Get error details if the environment is in an error state
379    ///
380    /// For error states (`*Failed`), this returns the description of the
381    /// operation that failed. For non-error states, returns `None`.
382    ///
383    /// # Returns
384    ///
385    /// - `Some(&str)` containing the failed operation description for error states
386    /// - `None` for success states
387    #[must_use]
388    pub fn error_details(&self) -> Option<&str> {
389        match self {
390            Self::ProvisionFailed(env) => Some(&env.state().context.base.error_summary),
391            Self::ConfigureFailed(env) => Some(&env.state().context.base.error_summary),
392            Self::ReleaseFailed(env) => Some(&env.state().context.base.error_summary),
393            Self::RunFailed(env) => Some(&env.state().context.base.error_summary),
394            Self::DestroyFailed(env) => Some(&env.state().context.base.error_summary),
395            _ => None,
396        }
397    }
398
399    /// Get the instance name regardless of current state
400    ///
401    /// This method provides access to the instance name without needing to
402    /// pattern match on the specific state variant.
403    ///
404    /// # Returns
405    ///
406    /// A reference to the `InstanceName` contained within the environment.
407    #[must_use]
408    pub fn instance_name(&self) -> &crate::domain::environment::InstanceName {
409        self.context().user_inputs.instance_name()
410    }
411
412    /// Get the LXD profile name regardless of current state
413    ///
414    /// This method provides access to the profile name without needing to
415    /// pattern match on the specific state variant.
416    ///
417    /// # Returns
418    ///
419    /// A reference to the `ProfileName` contained within the environment.
420    ///
421    /// # Panics
422    ///
423    /// Panics if called on a non-LXD environment.
424    #[must_use]
425    pub fn profile_name(&self) -> &crate::domain::environment::ProfileName {
426        &self
427            .context()
428            .user_inputs
429            .provider_config()
430            .as_lxd()
431            .expect("profile_name() called on non-LXD environment")
432            .profile_name
433    }
434
435    /// Get the SSH credentials regardless of current state
436    ///
437    /// This method provides access to the SSH credentials without needing to
438    /// pattern match on the specific state variant.
439    ///
440    /// # Returns
441    ///
442    /// A reference to the `SshCredentials` contained within the environment.
443    #[must_use]
444    pub fn ssh_credentials(&self) -> &crate::adapters::ssh::SshCredentials {
445        self.context().user_inputs.ssh_credentials()
446    }
447
448    /// Get the SSH port regardless of current state
449    ///
450    /// This method provides access to the SSH port without needing to
451    /// pattern match on the specific state variant.
452    ///
453    /// # Returns
454    ///
455    /// The SSH port number.
456    #[must_use]
457    pub fn ssh_port(&self) -> u16 {
458        self.context().user_inputs.ssh_port()
459    }
460
461    /// Get the provider name regardless of current state
462    ///
463    /// This method provides access to the provider name without needing to
464    /// pattern match on the specific state variant.
465    ///
466    /// # Returns
467    ///
468    /// A static string representing the provider name (e.g., "lxd", "hetzner").
469    #[must_use]
470    pub fn provider_name(&self) -> &'static str {
471        self.context().user_inputs.provider_config().provider_name()
472    }
473
474    /// Get the human-readable provider display name regardless of current state
475    ///
476    /// This method provides access to the provider display name without needing to
477    /// pattern match on the specific state variant.
478    ///
479    /// # Returns
480    ///
481    /// A static string representing the provider display name (e.g., "LXD", "Hetzner Cloud").
482    #[must_use]
483    pub fn provider_display_name(&self) -> &'static str {
484        self.context()
485            .user_inputs
486            .provider_config()
487            .provider_display_name()
488    }
489
490    /// Get the tracker configuration regardless of current state
491    ///
492    /// This method provides access to the tracker configuration without needing to
493    /// pattern match on the specific state variant.
494    ///
495    /// # Returns
496    ///
497    /// A reference to the `TrackerConfig` contained within the environment.
498    #[must_use]
499    pub fn tracker_config(&self) -> &crate::domain::tracker::TrackerConfig {
500        self.context().user_inputs.tracker()
501    }
502
503    /// Get the instance IP address if available, regardless of current state
504    ///
505    /// This method provides access to the instance IP without needing to
506    /// pattern match on the specific state variant.
507    ///
508    /// # Returns
509    ///
510    /// - `Some(IpAddr)` if the environment has been provisioned
511    /// - `None` if the environment hasn't been provisioned yet
512    #[must_use]
513    pub fn instance_ip(&self) -> Option<std::net::IpAddr> {
514        self.context().runtime_outputs.instance_ip()
515    }
516
517    /// Get when the environment was created
518    ///
519    /// This method provides access to the creation timestamp without needing to
520    /// pattern match on the specific state variant.
521    ///
522    /// # Returns
523    ///
524    /// The UTC timestamp when the environment was created.
525    #[must_use]
526    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
527        self.context().created_at
528    }
529
530    /// Get the provision method if available, regardless of current state
531    ///
532    /// This method provides access to the provision method without needing to
533    /// pattern match on the specific state variant.
534    ///
535    /// # Returns
536    ///
537    /// - `Some(ProvisionMethod::Provisioned)` if the instance was provisioned via `OpenTofu`
538    /// - `Some(ProvisionMethod::Registered)` if the instance was registered from existing infrastructure
539    /// - `None` if the provision method hasn't been set yet (legacy or pre-provisioned state)
540    #[must_use]
541    pub fn provision_method(&self) -> Option<ProvisionMethod> {
542        self.context().runtime_outputs.provision_method()
543    }
544
545    /// Get the service endpoints if available, regardless of current state
546    ///
547    /// This method provides access to the service endpoints without needing to
548    /// pattern match on the specific state variant.
549    ///
550    /// # Returns
551    ///
552    /// - `Some(&ServiceEndpoints)` if services have been started and URLs are available
553    /// - `None` if services haven't been started yet or URLs weren't recorded
554    #[must_use]
555    pub fn service_endpoints(&self) -> Option<&ServiceEndpoints> {
556        self.context().runtime_outputs.service_endpoints()
557    }
558
559    /// Get the Prometheus configuration if enabled, regardless of current state
560    ///
561    /// This method provides access to the Prometheus configuration without needing to
562    /// pattern match on the specific state variant.
563    ///
564    /// # Returns
565    ///
566    /// - `Some(&PrometheusConfig)` if Prometheus is configured for this environment
567    /// - `None` if Prometheus is not enabled
568    #[must_use]
569    pub fn prometheus_config(&self) -> Option<&crate::domain::prometheus::PrometheusConfig> {
570        self.context().user_inputs.prometheus()
571    }
572
573    /// Get the Grafana configuration if enabled, regardless of current state
574    ///
575    /// This method provides access to the Grafana configuration without needing to
576    /// pattern match on the specific state variant.
577    ///
578    /// # Returns
579    ///
580    /// - `Some(&GrafanaConfig)` if Grafana is configured for this environment
581    /// - `None` if Grafana is not enabled
582    #[must_use]
583    pub fn grafana_config(&self) -> Option<&crate::domain::grafana::GrafanaConfig> {
584        self.context().user_inputs.grafana()
585    }
586
587    /// Get the HTTPS configuration if enabled, regardless of current state
588    ///
589    /// This method provides access to the HTTPS configuration without needing to
590    /// pattern match on the specific state variant.
591    ///
592    /// # Returns
593    ///
594    /// - `Some(&HttpsConfig)` if HTTPS/TLS is configured for this environment
595    /// - `None` if HTTPS is not enabled
596    #[must_use]
597    pub fn https_config(&self) -> Option<&crate::domain::https::HttpsConfig> {
598        self.context().user_inputs.https()
599    }
600
601    /// Check if this environment was registered from existing infrastructure
602    ///
603    /// Registered environments have infrastructure that was created externally
604    /// and cannot be destroyed by this tool. The destroy command will only
605    /// clean up local state for registered environments.
606    ///
607    /// # Returns
608    ///
609    /// `true` if the environment was registered (not provisioned), `false` otherwise.
610    #[must_use]
611    pub fn is_registered(&self) -> bool {
612        matches!(self.provision_method(), Some(ProvisionMethod::Registered))
613    }
614
615    /// Collect all TLS-enabled domains from the environment configuration
616    ///
617    /// Gathers domains from all services that have TLS enabled:
618    /// HTTP API, HTTP trackers, health check API, and Grafana.
619    ///
620    /// This method is useful for operations that need to work with all
621    /// configured domains, such as DNS resolution checks, certificate
622    /// management, or reporting.
623    ///
624    /// # Returns
625    ///
626    /// A vector of all TLS domains configured in the environment.
627    /// Returns an empty vector if no TLS domains are configured.
628    #[must_use]
629    pub fn collect_tls_domains(&self) -> Vec<DomainName> {
630        let tracker_config = self.tracker_config();
631        let mut domains = Vec::new();
632
633        // HTTP API domain
634        if let Some(domain) = tracker_config.http_api().tls_domain() {
635            domains.push(domain.clone());
636        }
637
638        // HTTP tracker domains
639        for http_tracker in tracker_config.http_trackers() {
640            if let Some(domain) = http_tracker.tls_domain() {
641                domains.push(domain.clone());
642            }
643        }
644
645        // Health check API domain (returns &str, needs conversion)
646        if let Some(domain_str) = tracker_config.health_check_api().tls_domain() {
647            if let Ok(domain_name) = DomainName::new(domain_str) {
648                domains.push(domain_name);
649            }
650        }
651
652        // Grafana domain (returns &str, needs conversion)
653        if let Some(grafana_config) = self.grafana_config() {
654            if let Some(domain_str) = grafana_config.tls_domain() {
655                if let Ok(domain_name) = DomainName::new(domain_str) {
656                    domains.push(domain_name);
657                }
658            }
659        }
660
661        domains
662    }
663
664    /// Destroy the environment, transitioning it to the Destroyed state
665    ///
666    /// This method provides a unified interface to destroy an environment
667    /// regardless of its current state. It encapsulates the repetitive match
668    /// pattern that would otherwise be needed in calling code.
669    ///
670    /// # Returns
671    ///
672    /// - `Ok(Environment<Destroyed>)` if the environment was successfully destroyed
673    /// - `Err(StateTypeError)` if the environment is already in the `Destroyed` state
674    ///
675    /// # Errors
676    ///
677    /// Returns `StateTypeError::UnexpectedState` if called on an environment
678    /// already in the `Destroyed` state.
679    pub fn destroy(self) -> Result<Environment<Destroyed>, StateTypeError> {
680        match self {
681            Self::Created(env) => Ok(env.destroy()),
682            Self::Provisioning(env) => Ok(env.destroy()),
683            Self::Provisioned(env) => Ok(env.destroy()),
684            Self::Configuring(env) => Ok(env.destroy()),
685            Self::Configured(env) => Ok(env.destroy()),
686            Self::Releasing(env) => Ok(env.destroy()),
687            Self::Released(env) => Ok(env.destroy()),
688            Self::Running(env) => Ok(env.destroy()),
689            Self::Destroying(env) => Ok(env.destroy()),
690            Self::ProvisionFailed(env) => Ok(env.destroy()),
691            Self::ConfigureFailed(env) => Ok(env.destroy()),
692            Self::ReleaseFailed(env) => Ok(env.destroy()),
693            Self::RunFailed(env) => Ok(env.destroy()),
694            Self::DestroyFailed(env) => Ok(env.destroy()),
695            Self::Destroyed(_) => Err(StateTypeError::UnexpectedState {
696                expected: "any state except destroyed",
697                actual: "destroyed".to_string(),
698            }),
699        }
700    }
701
702    /// Get the `OpenTofu` build directory path regardless of current state
703    ///
704    /// This method provides a unified interface to access the build directory
705    /// for `OpenTofu` operations without needing to pattern match on the
706    /// specific state variant.
707    ///
708    /// The path is returned consistently regardless of the environment's state.
709    /// The caller is responsible for determining how to use the path based on
710    /// their specific needs and the environment's current state.
711    ///
712    /// # Returns
713    ///
714    /// The path to the `OpenTofu` build directory for the LXD provider.
715    #[must_use]
716    pub fn tofu_build_dir(&self) -> std::path::PathBuf {
717        self.context().tofu_build_dir()
718    }
719}
720
721/// Display implementation for user-friendly state representation
722///
723/// Formats the environment state in a human-readable way, including the
724/// environment name, current state, and error details if applicable.
725///
726/// # Examples
727///
728/// ```text
729/// Environment 'my-env' is in state: provisioning
730/// Environment 'my-env' is in state: provision_failed (failed at: network timeout)
731/// ```
732impl std::fmt::Display for AnyEnvironmentState {
733    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734        write!(
735            f,
736            "Environment '{}' is in state: {}",
737            self.name().as_str(),
738            self.state_name()
739        )?;
740
741        if let Some(error_details) = self.error_details() {
742            write!(f, " (failed at: {error_details})")?;
743        }
744
745        Ok(())
746    }
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752    use crate::adapters::ssh::SshCredentials;
753    use crate::domain::environment::name::EnvironmentName;
754    use crate::domain::provider::{LxdConfig, ProviderConfig};
755    use crate::domain::ProfileName;
756    use crate::shared::Username;
757    use std::path::PathBuf;
758
759    fn default_lxd_provider_config(env_name: &EnvironmentName) -> ProviderConfig {
760        ProviderConfig::Lxd(LxdConfig {
761            profile_name: ProfileName::new(format!("lxd-{}", env_name.as_str())).unwrap(),
762        })
763    }
764
765    /// Helper to create test SSH credentials
766    fn create_test_ssh_credentials() -> SshCredentials {
767        let username = Username::new("test-user".to_string()).unwrap();
768        SshCredentials::new(
769            PathBuf::from("/tmp/test_key"),
770            PathBuf::from("/tmp/test_key.pub"),
771            username,
772        )
773    }
774
775    /// Helper to create a test environment in Created state
776    fn create_test_environment_created() -> Environment<Created> {
777        let name = EnvironmentName::new("test-env".to_string()).unwrap();
778        let ssh_creds = create_test_ssh_credentials();
779        Environment::new(
780            name.clone(),
781            default_lxd_provider_config(&name),
782            ssh_creds,
783            22,
784            chrono::Utc::now(),
785        )
786    }
787
788    /// Helper to create a test `ProvisionFailureContext` with custom error message
789    fn create_test_provision_context(error_message: &str) -> ProvisionFailureContext {
790        use crate::domain::environment::TraceId;
791        use crate::shared::ErrorKind;
792        use chrono::Utc;
793        use std::time::Duration;
794
795        ProvisionFailureContext {
796            failed_step: ProvisionStep::OpenTofuApply,
797            error_kind: ErrorKind::InfrastructureOperation,
798            base: BaseFailureContext {
799                error_summary: error_message.to_string(),
800                failed_at: Utc::now(),
801                execution_started_at: Utc::now(),
802                execution_duration: Duration::from_secs(0),
803                trace_id: TraceId::default(),
804                trace_file_path: None,
805            },
806        }
807    }
808
809    /// Helper to create a test `ConfigureFailureContext` with custom error message
810    fn create_test_configure_context(error_message: &str) -> ConfigureFailureContext {
811        use crate::domain::environment::TraceId;
812        use crate::shared::ErrorKind;
813        use chrono::Utc;
814        use std::time::Duration;
815
816        ConfigureFailureContext {
817            failed_step: ConfigureStep::InstallDocker,
818            error_kind: ErrorKind::CommandExecution,
819            base: BaseFailureContext {
820                error_summary: error_message.to_string(),
821                failed_at: Utc::now(),
822                execution_started_at: Utc::now(),
823                execution_duration: Duration::from_secs(0),
824                trace_id: TraceId::default(),
825                trace_file_path: None,
826            },
827        }
828    }
829
830    /// Helper to create a test `ReleaseFailureContext` with custom error message
831    fn create_test_release_context(error_message: &str) -> ReleaseFailureContext {
832        use crate::domain::environment::TraceId;
833        use crate::shared::ErrorKind;
834        use chrono::Utc;
835        use std::time::Duration;
836
837        ReleaseFailureContext {
838            failed_step: ReleaseStep::DeployComposeFilesToRemote,
839            error_kind: ErrorKind::InfrastructureOperation,
840            base: BaseFailureContext {
841                error_summary: error_message.to_string(),
842                failed_at: Utc::now(),
843                execution_started_at: Utc::now(),
844                execution_duration: Duration::from_secs(0),
845                trace_id: TraceId::default(),
846                trace_file_path: None,
847            },
848        }
849    }
850
851    /// Helper to create a test `RunFailureContext` with custom error message
852    fn create_test_run_context(error_message: &str) -> RunFailureContext {
853        use crate::domain::environment::TraceId;
854        use crate::shared::ErrorKind;
855        use chrono::Utc;
856        use std::time::Duration;
857
858        RunFailureContext {
859            failed_step: RunStep::StartServices,
860            error_kind: ErrorKind::InfrastructureOperation,
861            base: BaseFailureContext {
862                error_summary: error_message.to_string(),
863                failed_at: Utc::now(),
864                execution_started_at: Utc::now(),
865                execution_duration: Duration::from_secs(0),
866                trace_id: TraceId::default(),
867                trace_file_path: None,
868            },
869        }
870    }
871
872    /// Test module for state marker types
873    ///
874    /// These tests verify that state types can be created, cloned, and serialized
875    /// correctly. They ensure basic functionality of the state marker types.
876
877    #[test]
878    fn it_should_create_provisioning_state() {
879        let state = Provisioning;
880        assert_eq!(state, Provisioning);
881    }
882
883    #[test]
884    fn it_should_create_provisioned_state() {
885        let state = Provisioned;
886        assert_eq!(state, Provisioned);
887    }
888
889    #[test]
890    fn it_should_create_configuring_state() {
891        let state = Configuring;
892        assert_eq!(state, Configuring);
893    }
894
895    #[test]
896    fn it_should_create_configured_state() {
897        let state = Configured;
898        assert_eq!(state, Configured);
899    }
900
901    #[test]
902    fn it_should_create_releasing_state() {
903        let state = Releasing;
904        assert_eq!(state, Releasing);
905    }
906
907    #[test]
908    fn it_should_create_released_state() {
909        let state = Released;
910        assert_eq!(state, Released);
911    }
912
913    #[test]
914    fn it_should_create_running_state() {
915        let state = Running;
916        assert_eq!(state, Running);
917    }
918
919    #[test]
920    fn it_should_create_provision_failed_state_with_context() {
921        let state = ProvisionFailed {
922            context: create_test_provision_context("cloud_init_execution"),
923        };
924        assert_eq!(state.context.base.error_summary, "cloud_init_execution");
925    }
926
927    #[test]
928    fn it_should_clone_provision_failed_state() {
929        let state = ProvisionFailed {
930            context: create_test_provision_context("cloud_init_execution"),
931        };
932        let cloned = state.clone();
933        assert_eq!(state, cloned);
934    }
935
936    #[test]
937    fn it_should_create_configure_failed_state_with_context() {
938        let state = ConfigureFailed {
939            context: create_test_configure_context("ansible_playbook_execution"),
940        };
941        assert_eq!(
942            state.context.base.error_summary,
943            "ansible_playbook_execution"
944        );
945    }
946
947    #[test]
948    fn it_should_create_release_failed_state_with_context() {
949        let context = create_test_release_context("build_artifacts");
950        let state = ReleaseFailed { context };
951        assert_eq!(state.context.base.error_summary, "build_artifacts");
952    }
953
954    #[test]
955    fn it_should_create_run_failed_state_with_context() {
956        let context = create_test_run_context("application_startup");
957        let state = RunFailed { context };
958        assert_eq!(state.context.base.error_summary, "application_startup");
959    }
960
961    #[test]
962    fn it_should_create_destroyed_state() {
963        let state = Destroyed;
964        assert_eq!(state, Destroyed);
965    }
966
967    #[test]
968    fn it_should_serialize_provision_failed_state_to_json() {
969        let state = ProvisionFailed {
970            context: create_test_provision_context("cloud_init"),
971        };
972        let json = serde_json::to_string(&state).unwrap();
973        assert!(json.contains("cloud_init"));
974        assert!(json.contains("context"));
975    }
976
977    #[test]
978    fn it_should_deserialize_provision_failed_state_from_json() {
979        // Note: This test now uses the full context structure
980        let context = create_test_provision_context("cloud_init");
981        let state = ProvisionFailed {
982            context: context.clone(),
983        };
984        let json = serde_json::to_string(&state).unwrap();
985        let deserialized: ProvisionFailed = serde_json::from_str(&json).unwrap();
986        assert_eq!(deserialized.context.base.error_summary, "cloud_init");
987    }
988
989    #[test]
990    fn it_should_serialize_configure_failed_state_to_json() {
991        let state = ConfigureFailed {
992            context: create_test_configure_context("ansible_playbook"),
993        };
994        let json = serde_json::to_string(&state).unwrap();
995        assert!(json.contains("InstallDocker"));
996        assert!(json.contains("CommandExecution"));
997        let deserialized: ConfigureFailed = serde_json::from_str(&json).unwrap();
998        assert_eq!(deserialized.context.base.error_summary, "ansible_playbook");
999    }
1000
1001    #[test]
1002    fn it_should_deserialize_configure_failed_state_from_json() {
1003        // Note: This test now uses the full context structure
1004        let context = create_test_configure_context("ansible_playbook");
1005        let state = ConfigureFailed {
1006            context: context.clone(),
1007        };
1008        let json = serde_json::to_string(&state).unwrap();
1009        let deserialized: ConfigureFailed = serde_json::from_str(&json).unwrap();
1010        assert_eq!(deserialized.context.base.error_summary, "ansible_playbook");
1011    }
1012
1013    // Tests for AnyEnvironmentState enum (Type Erasure)
1014    mod any_environment_state_tests {
1015        use std::net::{IpAddr, Ipv4Addr};
1016
1017        use super::*;
1018
1019        // Note: Helper functions for creating test environments and contexts
1020        // are defined in the parent module and can be accessed via super::
1021
1022        #[test]
1023        fn it_should_create_any_environment_state_with_created_variant() {
1024            let env = super::create_test_environment_created();
1025            let any_env = AnyEnvironmentState::Created(env);
1026            assert!(matches!(any_env, AnyEnvironmentState::Created(_)));
1027        }
1028
1029        // Note: Tests for other state variants will be added in Subtask 2
1030        // once we have the conversion methods (into_any()) that properly
1031        // create environments in different states through state transitions.
1032
1033        #[test]
1034        fn it_should_clone_any_environment_state() {
1035            let env = super::create_test_environment_created();
1036            let any_env = AnyEnvironmentState::Created(env);
1037            let cloned = any_env.clone();
1038            assert!(matches!(cloned, AnyEnvironmentState::Created(_)));
1039        }
1040
1041        #[test]
1042        fn it_should_debug_format_any_environment_state() {
1043            let env = super::create_test_environment_created();
1044            let any_env = AnyEnvironmentState::Created(env);
1045            let debug_str = format!("{any_env:?}");
1046            assert!(debug_str.contains("Created"));
1047        }
1048
1049        #[test]
1050        fn it_should_serialize_any_environment_state_to_json() {
1051            let env = super::create_test_environment_created();
1052            let any_env = AnyEnvironmentState::Created(env);
1053            let json = serde_json::to_string(&any_env).unwrap();
1054            assert!(json.contains("Created"));
1055        }
1056
1057        // Tests for Type Conversion Methods (Subtask 2)
1058
1059        // Tests for into_any() - Typed to Runtime conversions
1060
1061        #[test]
1062        fn it_should_convert_provisioning_environment_into_any() {
1063            let env = super::create_test_environment_created().start_provisioning();
1064            let any_env = env.into_any();
1065            assert!(matches!(any_env, AnyEnvironmentState::Provisioning(_)));
1066        }
1067
1068        #[test]
1069        fn it_should_convert_provisioned_environment_into_any() {
1070            let env = super::create_test_environment_created()
1071                .start_provisioning()
1072                .provisioned(
1073                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1074                    ProvisionMethod::Provisioned,
1075                );
1076            let any_env = env.into_any();
1077            assert!(matches!(any_env, AnyEnvironmentState::Provisioned(_)));
1078        }
1079
1080        #[test]
1081        fn it_should_convert_configuring_environment_into_any() {
1082            let env = super::create_test_environment_created()
1083                .start_provisioning()
1084                .provisioned(
1085                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1086                    ProvisionMethod::Provisioned,
1087                )
1088                .start_configuring();
1089            let any_env = env.into_any();
1090            assert!(matches!(any_env, AnyEnvironmentState::Configuring(_)));
1091        }
1092
1093        #[test]
1094        fn it_should_convert_configured_environment_into_any() {
1095            let env = super::create_test_environment_created()
1096                .start_provisioning()
1097                .provisioned(
1098                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1099                    ProvisionMethod::Provisioned,
1100                )
1101                .start_configuring()
1102                .configured();
1103            let any_env = env.into_any();
1104            assert!(matches!(any_env, AnyEnvironmentState::Configured(_)));
1105        }
1106
1107        #[test]
1108        fn it_should_convert_releasing_environment_into_any() {
1109            let env = super::create_test_environment_created()
1110                .start_provisioning()
1111                .provisioned(
1112                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1113                    ProvisionMethod::Provisioned,
1114                )
1115                .start_configuring()
1116                .configured()
1117                .start_releasing();
1118            let any_env = env.into_any();
1119            assert!(matches!(any_env, AnyEnvironmentState::Releasing(_)));
1120        }
1121
1122        #[test]
1123        fn it_should_convert_released_environment_into_any() {
1124            let env = super::create_test_environment_created()
1125                .start_provisioning()
1126                .provisioned(
1127                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1128                    ProvisionMethod::Provisioned,
1129                )
1130                .start_configuring()
1131                .configured()
1132                .start_releasing()
1133                .released();
1134            let any_env = env.into_any();
1135            assert!(matches!(any_env, AnyEnvironmentState::Released(_)));
1136        }
1137
1138        #[test]
1139        fn it_should_convert_running_environment_into_any() {
1140            let env = super::create_test_environment_created()
1141                .start_provisioning()
1142                .provisioned(
1143                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1144                    ProvisionMethod::Provisioned,
1145                )
1146                .start_configuring()
1147                .configured()
1148                .start_releasing()
1149                .released()
1150                .start_running();
1151            let any_env = env.into_any();
1152            assert!(matches!(any_env, AnyEnvironmentState::Running(_)));
1153        }
1154
1155        #[test]
1156        fn it_should_convert_provision_failed_environment_into_any() {
1157            let env = super::create_test_environment_created()
1158                .start_provisioning()
1159                .provision_failed(super::create_test_provision_context("infrastructure error"));
1160            let any_env = env.into_any();
1161            assert!(matches!(any_env, AnyEnvironmentState::ProvisionFailed(_)));
1162        }
1163
1164        #[test]
1165        fn it_should_convert_configure_failed_environment_into_any() {
1166            let env = super::create_test_environment_created()
1167                .start_provisioning()
1168                .provisioned(
1169                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1170                    ProvisionMethod::Provisioned,
1171                )
1172                .start_configuring()
1173                .configure_failed(super::create_test_configure_context("ansible error"));
1174            let any_env = env.into_any();
1175            assert!(matches!(any_env, AnyEnvironmentState::ConfigureFailed(_)));
1176        }
1177
1178        #[test]
1179        fn it_should_convert_release_failed_environment_into_any() {
1180            let env = super::create_test_environment_created()
1181                .start_provisioning()
1182                .provisioned(
1183                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1184                    ProvisionMethod::Provisioned,
1185                )
1186                .start_configuring()
1187                .configured()
1188                .start_releasing()
1189                .release_failed(super::create_test_release_context("release error"));
1190            let any_env = env.into_any();
1191            assert!(matches!(any_env, AnyEnvironmentState::ReleaseFailed(_)));
1192        }
1193
1194        #[test]
1195        fn it_should_convert_run_failed_environment_into_any() {
1196            let env = super::create_test_environment_created()
1197                .start_provisioning()
1198                .provisioned(
1199                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1200                    ProvisionMethod::Provisioned,
1201                )
1202                .start_configuring()
1203                .configured()
1204                .start_releasing()
1205                .released()
1206                .start_running()
1207                .run_failed(super::create_test_run_context("runtime error"));
1208            let any_env = env.into_any();
1209            assert!(matches!(any_env, AnyEnvironmentState::RunFailed(_)));
1210        }
1211
1212        #[test]
1213        fn it_should_convert_destroyed_environment_into_any() {
1214            let env = super::create_test_environment_created().destroy();
1215            let any_env = env.into_any();
1216            assert!(matches!(any_env, AnyEnvironmentState::Destroyed(_)));
1217        }
1218
1219        // Tests for try_into_<state>() - Runtime to Typed conversions (successful cases)
1220
1221        #[test]
1222        fn it_should_convert_any_to_provisioning_successfully() {
1223            let env = super::create_test_environment_created().start_provisioning();
1224            let any_env = env.into_any();
1225            let result = any_env.try_into_provisioning();
1226            assert!(result.is_ok());
1227        }
1228
1229        #[test]
1230        fn it_should_convert_any_to_provisioned_successfully() {
1231            let env = super::create_test_environment_created()
1232                .start_provisioning()
1233                .provisioned(
1234                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1235                    ProvisionMethod::Provisioned,
1236                );
1237            let any_env = env.into_any();
1238            let result = any_env.try_into_provisioned();
1239            assert!(result.is_ok());
1240        }
1241
1242        #[test]
1243        fn it_should_convert_any_to_configuring_successfully() {
1244            let env = super::create_test_environment_created()
1245                .start_provisioning()
1246                .provisioned(
1247                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1248                    ProvisionMethod::Provisioned,
1249                )
1250                .start_configuring();
1251            let any_env = env.into_any();
1252            let result = any_env.try_into_configuring();
1253            assert!(result.is_ok());
1254        }
1255
1256        #[test]
1257        fn it_should_convert_any_to_configured_successfully() {
1258            let env = super::create_test_environment_created()
1259                .start_provisioning()
1260                .provisioned(
1261                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1262                    ProvisionMethod::Provisioned,
1263                )
1264                .start_configuring()
1265                .configured();
1266            let any_env = env.into_any();
1267            let result = any_env.try_into_configured();
1268            assert!(result.is_ok());
1269        }
1270
1271        #[test]
1272        fn it_should_convert_any_to_releasing_successfully() {
1273            let env = super::create_test_environment_created()
1274                .start_provisioning()
1275                .provisioned(
1276                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1277                    ProvisionMethod::Provisioned,
1278                )
1279                .start_configuring()
1280                .configured()
1281                .start_releasing();
1282            let any_env = env.into_any();
1283            let result = any_env.try_into_releasing();
1284            assert!(result.is_ok());
1285        }
1286
1287        #[test]
1288        fn it_should_convert_any_to_released_successfully() {
1289            let env = super::create_test_environment_created()
1290                .start_provisioning()
1291                .provisioned(
1292                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1293                    ProvisionMethod::Provisioned,
1294                )
1295                .start_configuring()
1296                .configured()
1297                .start_releasing()
1298                .released();
1299            let any_env = env.into_any();
1300            let result = any_env.try_into_released();
1301            assert!(result.is_ok());
1302        }
1303
1304        #[test]
1305        fn it_should_convert_any_to_running_successfully() {
1306            let env = super::create_test_environment_created()
1307                .start_provisioning()
1308                .provisioned(
1309                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1310                    ProvisionMethod::Provisioned,
1311                )
1312                .start_configuring()
1313                .configured()
1314                .start_releasing()
1315                .released()
1316                .start_running();
1317            let any_env = env.into_any();
1318            let result = any_env.try_into_running();
1319            assert!(result.is_ok());
1320        }
1321
1322        #[test]
1323        fn it_should_convert_any_to_provision_failed_successfully() {
1324            let env = super::create_test_environment_created()
1325                .start_provisioning()
1326                .provision_failed(super::create_test_provision_context("test error"));
1327            let any_env = env.into_any();
1328            let result = any_env.try_into_provision_failed();
1329            assert!(result.is_ok());
1330        }
1331
1332        #[test]
1333        fn it_should_convert_any_to_configure_failed_successfully() {
1334            let env = super::create_test_environment_created()
1335                .start_provisioning()
1336                .provisioned(
1337                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1338                    ProvisionMethod::Provisioned,
1339                )
1340                .start_configuring()
1341                .configure_failed(super::create_test_configure_context("test error"));
1342            let any_env = env.into_any();
1343            let result = any_env.try_into_configure_failed();
1344            assert!(result.is_ok());
1345        }
1346
1347        #[test]
1348        fn it_should_convert_any_to_release_failed_successfully() {
1349            let env = super::create_test_environment_created()
1350                .start_provisioning()
1351                .provisioned(
1352                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1353                    ProvisionMethod::Provisioned,
1354                )
1355                .start_configuring()
1356                .configured()
1357                .start_releasing()
1358                .release_failed(super::create_test_release_context("test error"));
1359            let any_env = env.into_any();
1360            let result = any_env.try_into_release_failed();
1361            assert!(result.is_ok());
1362        }
1363
1364        #[test]
1365        fn it_should_convert_any_to_run_failed_successfully() {
1366            let env = super::create_test_environment_created()
1367                .start_provisioning()
1368                .provisioned(
1369                    IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1370                    ProvisionMethod::Provisioned,
1371                )
1372                .start_configuring()
1373                .configured()
1374                .start_releasing()
1375                .released()
1376                .start_running()
1377                .run_failed(super::create_test_run_context("test error"));
1378            let any_env = env.into_any();
1379            let result = any_env.try_into_run_failed();
1380            assert!(result.is_ok());
1381        }
1382
1383        #[test]
1384        fn it_should_convert_any_to_destroyed_successfully() {
1385            let env = super::create_test_environment_created().destroy();
1386            let any_env = env.into_any();
1387            let result = any_env.try_into_destroyed();
1388            assert!(result.is_ok());
1389        }
1390
1391        // Tests for try_into_<state>() - Runtime to Typed conversions (failure cases)
1392
1393        #[test]
1394        fn it_should_fail_converting_created_to_provisioning() {
1395            let env = super::create_test_environment_created();
1396            let any_env = env.into_any();
1397            let result = any_env.try_into_provisioning();
1398            assert!(result.is_err());
1399            let err = result.unwrap_err();
1400            assert!(err.to_string().contains("provisioning"));
1401            assert!(err.to_string().contains("created"));
1402        }
1403
1404        #[test]
1405        fn it_should_fail_converting_provision_failed_to_provisioned() {
1406            let env = super::create_test_environment_created()
1407                .start_provisioning()
1408                .provision_failed(super::create_test_provision_context("error"));
1409            let any_env = env.into_any();
1410            let result = any_env.try_into_provisioned();
1411            assert!(result.is_err());
1412            let err = result.unwrap_err();
1413            assert!(err.to_string().contains("provisioned"));
1414            assert!(err.to_string().contains("provision_failed"));
1415        }
1416
1417        #[test]
1418        fn it_should_fail_converting_destroyed_to_running() {
1419            let env = super::create_test_environment_created().destroy();
1420            let any_env = env.into_any();
1421            let result = any_env.try_into_running();
1422            assert!(result.is_err());
1423            let err = result.unwrap_err();
1424            assert!(err.to_string().contains("running"));
1425            assert!(err.to_string().contains("destroyed"));
1426        }
1427
1428        // Tests for round-trip conversions (preserving data integrity)
1429
1430        #[test]
1431        fn it_should_preserve_error_details_in_failed_states() {
1432            let error_message = "infrastructure deployment failed";
1433            let context = super::create_test_provision_context(error_message);
1434            let env = super::create_test_environment_created()
1435                .start_provisioning()
1436                .provision_failed(context.clone());
1437
1438            // Round-trip conversion
1439            let any_env = env.into_any();
1440            let env_restored = any_env.try_into_provision_failed().unwrap();
1441
1442            assert_eq!(
1443                env_restored.state().context.base.error_summary,
1444                error_message
1445            );
1446        }
1447
1448        // Tests for new utility methods
1449
1450        #[test]
1451        fn it_should_destroy_environment_from_any_state() {
1452            let env = super::create_test_environment_created();
1453            let any_env = AnyEnvironmentState::Created(env);
1454
1455            let destroyed = any_env.destroy().unwrap();
1456            // Convert back to any state to check state name
1457            let destroyed_any = destroyed.into_any();
1458            assert_eq!(destroyed_any.state_name(), "destroyed");
1459        }
1460
1461        #[test]
1462        fn it_should_get_tofu_build_dir_from_any_state() {
1463            let env = super::create_test_environment_created();
1464            let any_env = AnyEnvironmentState::Created(env);
1465
1466            let build_dir = any_env.tofu_build_dir();
1467            assert!(build_dir.ends_with("lxd"));
1468        }
1469
1470        #[test]
1471        fn it_should_error_when_destroying_already_destroyed_environment() {
1472            let env = super::create_test_environment_created().destroy();
1473            let any_env = AnyEnvironmentState::Destroyed(env);
1474
1475            // This should return an error
1476            let result = any_env.destroy();
1477            assert!(result.is_err());
1478            let error = result.unwrap_err();
1479            assert_eq!(
1480                error.to_string(),
1481                "Expected state 'any state except destroyed', but found 'destroyed'"
1482            );
1483        }
1484
1485        #[test]
1486        fn it_should_get_tofu_build_dir_from_destroyed_environment() {
1487            let env = super::create_test_environment_created().destroy();
1488            let any_env = AnyEnvironmentState::Destroyed(env);
1489
1490            // This should now always return the path, even for destroyed environments
1491            let build_dir = any_env.tofu_build_dir();
1492            assert!(build_dir.ends_with("lxd"));
1493        }
1494    }
1495
1496    mod introspection_tests {
1497        use super::{
1498            create_test_configure_context, create_test_environment_created,
1499            create_test_provision_context,
1500        };
1501
1502        mod name {
1503            use super::super::EnvironmentName;
1504
1505            #[test]
1506            fn it_should_return_environment_name_for_created_state() {
1507                let any_env = super::create_test_environment_created().into_any();
1508                let env_name = EnvironmentName::new("test-env".to_string()).unwrap();
1509
1510                assert_eq!(any_env.name(), &env_name);
1511                assert_eq!(any_env.name().as_str(), "test-env");
1512            }
1513
1514            #[test]
1515            fn it_should_return_same_name_for_provisioning_state() {
1516                let any_env = super::create_test_environment_created()
1517                    .start_provisioning()
1518                    .into_any();
1519                let env_name = EnvironmentName::new("test-env".to_string()).unwrap();
1520
1521                assert_eq!(any_env.name(), &env_name);
1522            }
1523
1524            #[test]
1525            fn it_should_return_same_name_for_error_states() {
1526                let any_env = super::create_test_environment_created()
1527                    .start_provisioning()
1528                    .provision_failed(super::create_test_provision_context("error"))
1529                    .into_any();
1530                let env_name = EnvironmentName::new("test-env".to_string()).unwrap();
1531
1532                assert_eq!(any_env.name(), &env_name);
1533            }
1534        }
1535
1536        mod state_name {
1537            use std::net::{IpAddr, Ipv4Addr};
1538
1539            use super::super::ProvisionMethod;
1540
1541            #[test]
1542            fn it_should_return_created_for_created_state() {
1543                let any_env = super::create_test_environment_created().into_any();
1544                assert_eq!(any_env.state_name(), "created");
1545            }
1546
1547            #[test]
1548            fn it_should_return_provisioning_for_provisioning_state() {
1549                let any_env = super::create_test_environment_created()
1550                    .start_provisioning()
1551                    .into_any();
1552                assert_eq!(any_env.state_name(), "provisioning");
1553            }
1554
1555            #[test]
1556            fn it_should_return_provisioned_for_provisioned_state() {
1557                let any_env = super::create_test_environment_created()
1558                    .start_provisioning()
1559                    .provisioned(
1560                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1561                        ProvisionMethod::Provisioned,
1562                    )
1563                    .into_any();
1564                assert_eq!(any_env.state_name(), "provisioned");
1565            }
1566
1567            #[test]
1568            fn it_should_return_configuring_for_configuring_state() {
1569                let any_env = super::create_test_environment_created()
1570                    .start_provisioning()
1571                    .provisioned(
1572                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1573                        ProvisionMethod::Provisioned,
1574                    )
1575                    .start_configuring()
1576                    .into_any();
1577                assert_eq!(any_env.state_name(), "configuring");
1578            }
1579
1580            #[test]
1581            fn it_should_return_configured_for_configured_state() {
1582                let any_env = super::create_test_environment_created()
1583                    .start_provisioning()
1584                    .provisioned(
1585                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1586                        ProvisionMethod::Provisioned,
1587                    )
1588                    .start_configuring()
1589                    .configured()
1590                    .into_any();
1591                assert_eq!(any_env.state_name(), "configured");
1592            }
1593
1594            #[test]
1595            fn it_should_return_releasing_for_releasing_state() {
1596                let any_env = super::create_test_environment_created()
1597                    .start_provisioning()
1598                    .provisioned(
1599                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1600                        ProvisionMethod::Provisioned,
1601                    )
1602                    .start_configuring()
1603                    .configured()
1604                    .start_releasing()
1605                    .into_any();
1606                assert_eq!(any_env.state_name(), "releasing");
1607            }
1608
1609            #[test]
1610            fn it_should_return_released_for_released_state() {
1611                let any_env = super::create_test_environment_created()
1612                    .start_provisioning()
1613                    .provisioned(
1614                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1615                        ProvisionMethod::Provisioned,
1616                    )
1617                    .start_configuring()
1618                    .configured()
1619                    .start_releasing()
1620                    .released()
1621                    .into_any();
1622                assert_eq!(any_env.state_name(), "released");
1623            }
1624
1625            #[test]
1626            fn it_should_return_running_for_running_state() {
1627                let any_env = super::create_test_environment_created()
1628                    .start_provisioning()
1629                    .provisioned(
1630                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1631                        ProvisionMethod::Provisioned,
1632                    )
1633                    .start_configuring()
1634                    .configured()
1635                    .start_releasing()
1636                    .released()
1637                    .start_running()
1638                    .into_any();
1639                assert_eq!(any_env.state_name(), "running");
1640            }
1641
1642            #[test]
1643            fn it_should_return_provision_failed_for_provision_failed_state() {
1644                let any_env = super::create_test_environment_created()
1645                    .start_provisioning()
1646                    .provision_failed(super::create_test_provision_context("error"))
1647                    .into_any();
1648                assert_eq!(any_env.state_name(), "provision_failed");
1649            }
1650
1651            #[test]
1652            fn it_should_return_configure_failed_for_configure_failed_state() {
1653                let any_env = super::create_test_environment_created()
1654                    .start_provisioning()
1655                    .provisioned(
1656                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1657                        ProvisionMethod::Provisioned,
1658                    )
1659                    .start_configuring()
1660                    .configure_failed(super::create_test_configure_context("error"))
1661                    .into_any();
1662                assert_eq!(any_env.state_name(), "configure_failed");
1663            }
1664
1665            #[test]
1666            fn it_should_return_release_failed_for_release_failed_state() {
1667                let any_env = super::create_test_environment_created()
1668                    .start_provisioning()
1669                    .provisioned(
1670                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1671                        ProvisionMethod::Provisioned,
1672                    )
1673                    .start_configuring()
1674                    .configured()
1675                    .start_releasing()
1676                    .release_failed(super::super::create_test_release_context("error"))
1677                    .into_any();
1678                assert_eq!(any_env.state_name(), "release_failed");
1679            }
1680
1681            #[test]
1682            fn it_should_return_run_failed_for_run_failed_state() {
1683                let any_env = super::create_test_environment_created()
1684                    .start_provisioning()
1685                    .provisioned(
1686                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1687                        ProvisionMethod::Provisioned,
1688                    )
1689                    .start_configuring()
1690                    .configured()
1691                    .start_releasing()
1692                    .released()
1693                    .start_running()
1694                    .run_failed(super::super::create_test_run_context("error"))
1695                    .into_any();
1696                assert_eq!(any_env.state_name(), "run_failed");
1697            }
1698
1699            #[test]
1700            fn it_should_return_destroyed_for_destroyed_state() {
1701                let any_env = super::create_test_environment_created()
1702                    .destroy()
1703                    .into_any();
1704                assert_eq!(any_env.state_name(), "destroyed");
1705            }
1706        }
1707
1708        mod is_success_state {
1709            use std::net::{IpAddr, Ipv4Addr};
1710
1711            use super::super::ProvisionMethod;
1712
1713            #[test]
1714            fn it_should_return_true_for_created_state() {
1715                let any_env = super::create_test_environment_created().into_any();
1716                assert!(any_env.is_success_state());
1717            }
1718
1719            #[test]
1720            fn it_should_return_true_for_provisioning_state() {
1721                let any_env = super::create_test_environment_created()
1722                    .start_provisioning()
1723                    .into_any();
1724                assert!(any_env.is_success_state());
1725            }
1726
1727            #[test]
1728            fn it_should_return_true_for_provisioned_state() {
1729                let any_env = super::create_test_environment_created()
1730                    .start_provisioning()
1731                    .provisioned(
1732                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1733                        ProvisionMethod::Provisioned,
1734                    )
1735                    .into_any();
1736                assert!(any_env.is_success_state());
1737            }
1738
1739            #[test]
1740            fn it_should_return_true_for_configuring_state() {
1741                let any_env = super::create_test_environment_created()
1742                    .start_provisioning()
1743                    .provisioned(
1744                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1745                        ProvisionMethod::Provisioned,
1746                    )
1747                    .start_configuring()
1748                    .into_any();
1749                assert!(any_env.is_success_state());
1750            }
1751
1752            #[test]
1753            fn it_should_return_true_for_configured_state() {
1754                let any_env = super::create_test_environment_created()
1755                    .start_provisioning()
1756                    .provisioned(
1757                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1758                        ProvisionMethod::Provisioned,
1759                    )
1760                    .start_configuring()
1761                    .configured()
1762                    .into_any();
1763                assert!(any_env.is_success_state());
1764            }
1765
1766            #[test]
1767            fn it_should_return_true_for_releasing_state() {
1768                let any_env = super::create_test_environment_created()
1769                    .start_provisioning()
1770                    .provisioned(
1771                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1772                        ProvisionMethod::Provisioned,
1773                    )
1774                    .start_configuring()
1775                    .configured()
1776                    .start_releasing()
1777                    .into_any();
1778                assert!(any_env.is_success_state());
1779            }
1780
1781            #[test]
1782            fn it_should_return_true_for_released_state() {
1783                let any_env = super::create_test_environment_created()
1784                    .start_provisioning()
1785                    .provisioned(
1786                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1787                        ProvisionMethod::Provisioned,
1788                    )
1789                    .start_configuring()
1790                    .configured()
1791                    .start_releasing()
1792                    .released()
1793                    .into_any();
1794                assert!(any_env.is_success_state());
1795            }
1796
1797            #[test]
1798            fn it_should_return_true_for_running_state() {
1799                let any_env = super::create_test_environment_created()
1800                    .start_provisioning()
1801                    .provisioned(
1802                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1803                        ProvisionMethod::Provisioned,
1804                    )
1805                    .start_configuring()
1806                    .configured()
1807                    .start_releasing()
1808                    .released()
1809                    .start_running()
1810                    .into_any();
1811                assert!(any_env.is_success_state());
1812            }
1813
1814            #[test]
1815            fn it_should_return_true_for_destroyed_state() {
1816                let any_env = super::create_test_environment_created()
1817                    .destroy()
1818                    .into_any();
1819                assert!(any_env.is_success_state());
1820            }
1821
1822            #[test]
1823            fn it_should_return_false_for_provision_failed_state() {
1824                let any_env = super::create_test_environment_created()
1825                    .start_provisioning()
1826                    .provision_failed(super::create_test_provision_context("error"))
1827                    .into_any();
1828                assert!(!any_env.is_success_state());
1829            }
1830
1831            #[test]
1832            fn it_should_return_false_for_configure_failed_state() {
1833                let any_env = super::create_test_environment_created()
1834                    .start_provisioning()
1835                    .provisioned(
1836                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1837                        ProvisionMethod::Provisioned,
1838                    )
1839                    .start_configuring()
1840                    .configure_failed(super::create_test_configure_context("error"))
1841                    .into_any();
1842                assert!(!any_env.is_success_state());
1843            }
1844
1845            #[test]
1846            fn it_should_return_false_for_release_failed_state() {
1847                let any_env = super::create_test_environment_created()
1848                    .start_provisioning()
1849                    .provisioned(
1850                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1851                        ProvisionMethod::Provisioned,
1852                    )
1853                    .start_configuring()
1854                    .configured()
1855                    .start_releasing()
1856                    .release_failed(super::super::create_test_release_context("error"))
1857                    .into_any();
1858                assert!(!any_env.is_success_state());
1859            }
1860
1861            #[test]
1862            fn it_should_return_false_for_run_failed_state() {
1863                let any_env = super::create_test_environment_created()
1864                    .start_provisioning()
1865                    .provisioned(
1866                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1867                        ProvisionMethod::Provisioned,
1868                    )
1869                    .start_configuring()
1870                    .configured()
1871                    .start_releasing()
1872                    .released()
1873                    .start_running()
1874                    .run_failed(super::super::create_test_run_context("error"))
1875                    .into_any();
1876                assert!(!any_env.is_success_state());
1877            }
1878        }
1879
1880        mod is_error_state {
1881            use std::net::{IpAddr, Ipv4Addr};
1882
1883            use super::super::ProvisionMethod;
1884
1885            #[test]
1886            fn it_should_return_false_for_success_states() {
1887                let success_states = vec![
1888                    super::create_test_environment_created().into_any(),
1889                    super::create_test_environment_created()
1890                        .start_provisioning()
1891                        .into_any(),
1892                    super::create_test_environment_created()
1893                        .start_provisioning()
1894                        .provisioned(
1895                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1896                            ProvisionMethod::Provisioned,
1897                        )
1898                        .into_any(),
1899                    super::create_test_environment_created()
1900                        .start_provisioning()
1901                        .provisioned(
1902                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1903                            ProvisionMethod::Provisioned,
1904                        )
1905                        .start_configuring()
1906                        .into_any(),
1907                    super::create_test_environment_created()
1908                        .start_provisioning()
1909                        .provisioned(
1910                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1911                            ProvisionMethod::Provisioned,
1912                        )
1913                        .start_configuring()
1914                        .configured()
1915                        .into_any(),
1916                    super::create_test_environment_created()
1917                        .start_provisioning()
1918                        .provisioned(
1919                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1920                            ProvisionMethod::Provisioned,
1921                        )
1922                        .start_configuring()
1923                        .configured()
1924                        .start_releasing()
1925                        .into_any(),
1926                    super::create_test_environment_created()
1927                        .start_provisioning()
1928                        .provisioned(
1929                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1930                            ProvisionMethod::Provisioned,
1931                        )
1932                        .start_configuring()
1933                        .configured()
1934                        .start_releasing()
1935                        .released()
1936                        .into_any(),
1937                    super::create_test_environment_created()
1938                        .start_provisioning()
1939                        .provisioned(
1940                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1941                            ProvisionMethod::Provisioned,
1942                        )
1943                        .start_configuring()
1944                        .configured()
1945                        .start_releasing()
1946                        .released()
1947                        .start_running()
1948                        .into_any(),
1949                    super::create_test_environment_created()
1950                        .destroy()
1951                        .into_any(),
1952                ];
1953
1954                for state in success_states {
1955                    assert!(!state.is_error_state());
1956                }
1957            }
1958
1959            #[test]
1960            fn it_should_return_true_for_provision_failed_state() {
1961                let any_env = super::create_test_environment_created()
1962                    .start_provisioning()
1963                    .provision_failed(super::create_test_provision_context("error"))
1964                    .into_any();
1965                assert!(any_env.is_error_state());
1966            }
1967
1968            #[test]
1969            fn it_should_return_true_for_configure_failed_state() {
1970                let any_env = super::create_test_environment_created()
1971                    .start_provisioning()
1972                    .provisioned(
1973                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1974                        ProvisionMethod::Provisioned,
1975                    )
1976                    .start_configuring()
1977                    .configure_failed(super::create_test_configure_context("error"))
1978                    .into_any();
1979                assert!(any_env.is_error_state());
1980            }
1981
1982            #[test]
1983            fn it_should_return_true_for_release_failed_state() {
1984                let any_env = super::create_test_environment_created()
1985                    .start_provisioning()
1986                    .provisioned(
1987                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
1988                        ProvisionMethod::Provisioned,
1989                    )
1990                    .start_configuring()
1991                    .configured()
1992                    .start_releasing()
1993                    .release_failed(super::super::create_test_release_context("error"))
1994                    .into_any();
1995                assert!(any_env.is_error_state());
1996            }
1997
1998            #[test]
1999            fn it_should_return_true_for_run_failed_state() {
2000                let any_env = super::create_test_environment_created()
2001                    .start_provisioning()
2002                    .provisioned(
2003                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2004                        ProvisionMethod::Provisioned,
2005                    )
2006                    .start_configuring()
2007                    .configured()
2008                    .start_releasing()
2009                    .released()
2010                    .start_running()
2011                    .run_failed(super::super::create_test_run_context("error"))
2012                    .into_any();
2013                assert!(any_env.is_error_state());
2014            }
2015        }
2016
2017        mod is_terminal_state {
2018            use std::net::{IpAddr, Ipv4Addr};
2019
2020            use super::super::ProvisionMethod;
2021
2022            #[test]
2023            fn it_should_return_false_for_transient_states() {
2024                let transient_states = vec![
2025                    super::create_test_environment_created().into_any(),
2026                    super::create_test_environment_created()
2027                        .start_provisioning()
2028                        .into_any(),
2029                    super::create_test_environment_created()
2030                        .start_provisioning()
2031                        .provisioned(
2032                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2033                            ProvisionMethod::Provisioned,
2034                        )
2035                        .into_any(),
2036                    super::create_test_environment_created()
2037                        .start_provisioning()
2038                        .provisioned(
2039                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2040                            ProvisionMethod::Provisioned,
2041                        )
2042                        .start_configuring()
2043                        .into_any(),
2044                    super::create_test_environment_created()
2045                        .start_provisioning()
2046                        .provisioned(
2047                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2048                            ProvisionMethod::Provisioned,
2049                        )
2050                        .start_configuring()
2051                        .configured()
2052                        .into_any(),
2053                    super::create_test_environment_created()
2054                        .start_provisioning()
2055                        .provisioned(
2056                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2057                            ProvisionMethod::Provisioned,
2058                        )
2059                        .start_configuring()
2060                        .configured()
2061                        .start_releasing()
2062                        .into_any(),
2063                    super::create_test_environment_created()
2064                        .start_provisioning()
2065                        .provisioned(
2066                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2067                            ProvisionMethod::Provisioned,
2068                        )
2069                        .start_configuring()
2070                        .configured()
2071                        .start_releasing()
2072                        .released()
2073                        .into_any(),
2074                ];
2075
2076                for state in transient_states {
2077                    assert!(!state.is_terminal_state());
2078                }
2079            }
2080
2081            #[test]
2082            fn it_should_return_true_for_running_state() {
2083                let any_env = super::create_test_environment_created()
2084                    .start_provisioning()
2085                    .provisioned(
2086                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2087                        ProvisionMethod::Provisioned,
2088                    )
2089                    .start_configuring()
2090                    .configured()
2091                    .start_releasing()
2092                    .released()
2093                    .start_running()
2094                    .into_any();
2095                assert!(any_env.is_terminal_state());
2096            }
2097
2098            #[test]
2099            fn it_should_return_true_for_destroyed_state() {
2100                let any_env = super::create_test_environment_created()
2101                    .destroy()
2102                    .into_any();
2103                assert!(any_env.is_terminal_state());
2104            }
2105
2106            #[test]
2107            fn it_should_return_true_for_provision_failed_state() {
2108                let any_env = super::create_test_environment_created()
2109                    .start_provisioning()
2110                    .provision_failed(super::create_test_provision_context("error"))
2111                    .into_any();
2112                assert!(any_env.is_terminal_state());
2113            }
2114
2115            #[test]
2116            fn it_should_return_true_for_configure_failed_state() {
2117                let any_env = super::create_test_environment_created()
2118                    .start_provisioning()
2119                    .provisioned(
2120                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2121                        ProvisionMethod::Provisioned,
2122                    )
2123                    .start_configuring()
2124                    .configure_failed(super::create_test_configure_context("error"))
2125                    .into_any();
2126                assert!(any_env.is_terminal_state());
2127            }
2128
2129            #[test]
2130            fn it_should_return_true_for_release_failed_state() {
2131                let any_env = super::create_test_environment_created()
2132                    .start_provisioning()
2133                    .provisioned(
2134                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2135                        ProvisionMethod::Provisioned,
2136                    )
2137                    .start_configuring()
2138                    .configured()
2139                    .start_releasing()
2140                    .release_failed(super::super::create_test_release_context("error"))
2141                    .into_any();
2142                assert!(any_env.is_terminal_state());
2143            }
2144
2145            #[test]
2146            fn it_should_return_true_for_run_failed_state() {
2147                let any_env = super::create_test_environment_created()
2148                    .start_provisioning()
2149                    .provisioned(
2150                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2151                        ProvisionMethod::Provisioned,
2152                    )
2153                    .start_configuring()
2154                    .configured()
2155                    .start_releasing()
2156                    .released()
2157                    .start_running()
2158                    .run_failed(super::super::create_test_run_context("error"))
2159                    .into_any();
2160                assert!(any_env.is_terminal_state());
2161            }
2162        }
2163
2164        mod error_details {
2165            use std::net::{IpAddr, Ipv4Addr};
2166
2167            use super::super::ProvisionMethod;
2168
2169            #[test]
2170            fn it_should_return_none_for_success_states() {
2171                let success_states = vec![
2172                    super::create_test_environment_created().into_any(),
2173                    super::create_test_environment_created()
2174                        .start_provisioning()
2175                        .into_any(),
2176                    super::create_test_environment_created()
2177                        .start_provisioning()
2178                        .provisioned(
2179                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2180                            ProvisionMethod::Provisioned,
2181                        )
2182                        .into_any(),
2183                    super::create_test_environment_created()
2184                        .start_provisioning()
2185                        .provisioned(
2186                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2187                            ProvisionMethod::Provisioned,
2188                        )
2189                        .start_configuring()
2190                        .into_any(),
2191                    super::create_test_environment_created()
2192                        .start_provisioning()
2193                        .provisioned(
2194                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2195                            ProvisionMethod::Provisioned,
2196                        )
2197                        .start_configuring()
2198                        .configured()
2199                        .into_any(),
2200                    super::create_test_environment_created()
2201                        .start_provisioning()
2202                        .provisioned(
2203                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2204                            ProvisionMethod::Provisioned,
2205                        )
2206                        .start_configuring()
2207                        .configured()
2208                        .start_releasing()
2209                        .into_any(),
2210                    super::create_test_environment_created()
2211                        .start_provisioning()
2212                        .provisioned(
2213                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2214                            ProvisionMethod::Provisioned,
2215                        )
2216                        .start_configuring()
2217                        .configured()
2218                        .start_releasing()
2219                        .released()
2220                        .into_any(),
2221                    super::create_test_environment_created()
2222                        .start_provisioning()
2223                        .provisioned(
2224                            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2225                            ProvisionMethod::Provisioned,
2226                        )
2227                        .start_configuring()
2228                        .configured()
2229                        .start_releasing()
2230                        .released()
2231                        .start_running()
2232                        .into_any(),
2233                    super::create_test_environment_created()
2234                        .destroy()
2235                        .into_any(),
2236                ];
2237
2238                for state in success_states {
2239                    assert!(state.error_details().is_none());
2240                }
2241            }
2242
2243            #[test]
2244            fn it_should_return_error_message_for_provision_failed_state() {
2245                let error_message = "network timeout during provisioning";
2246                let any_env = super::create_test_environment_created()
2247                    .start_provisioning()
2248                    .provision_failed(super::create_test_provision_context(error_message))
2249                    .into_any();
2250
2251                assert_eq!(any_env.error_details(), Some(error_message));
2252            }
2253
2254            #[test]
2255            fn it_should_return_error_message_for_configure_failed_state() {
2256                let error_message = "ansible playbook failed";
2257                let any_env = super::create_test_environment_created()
2258                    .start_provisioning()
2259                    .provisioned(
2260                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2261                        ProvisionMethod::Provisioned,
2262                    )
2263                    .start_configuring()
2264                    .configure_failed(super::create_test_configure_context(error_message))
2265                    .into_any();
2266
2267                assert_eq!(any_env.error_details(), Some(error_message));
2268            }
2269
2270            #[test]
2271            fn it_should_return_error_message_for_release_failed_state() {
2272                let error_message = "release process error";
2273                let any_env = super::create_test_environment_created()
2274                    .start_provisioning()
2275                    .provisioned(
2276                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2277                        ProvisionMethod::Provisioned,
2278                    )
2279                    .start_configuring()
2280                    .configured()
2281                    .start_releasing()
2282                    .release_failed(super::super::create_test_release_context(error_message))
2283                    .into_any();
2284
2285                assert_eq!(any_env.error_details(), Some(error_message));
2286            }
2287
2288            #[test]
2289            fn it_should_return_error_message_for_run_failed_state() {
2290                let error_message = "application crash";
2291                let any_env = super::create_test_environment_created()
2292                    .start_provisioning()
2293                    .provisioned(
2294                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2295                        ProvisionMethod::Provisioned,
2296                    )
2297                    .start_configuring()
2298                    .configured()
2299                    .start_releasing()
2300                    .released()
2301                    .start_running()
2302                    .run_failed(super::super::create_test_run_context(error_message))
2303                    .into_any();
2304
2305                assert_eq!(any_env.error_details(), Some(error_message));
2306            }
2307        }
2308
2309        mod display {
2310            use std::net::{IpAddr, Ipv4Addr};
2311
2312            use super::super::ProvisionMethod;
2313
2314            #[test]
2315            fn it_should_format_success_state_without_error_details() {
2316                let any_env = super::create_test_environment_created()
2317                    .start_provisioning()
2318                    .into_any();
2319
2320                let output = format!("{any_env}");
2321                assert_eq!(output, "Environment 'test-env' is in state: provisioning");
2322            }
2323
2324            #[test]
2325            fn it_should_format_running_state() {
2326                let any_env = super::create_test_environment_created()
2327                    .start_provisioning()
2328                    .provisioned(
2329                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2330                        ProvisionMethod::Provisioned,
2331                    )
2332                    .start_configuring()
2333                    .configured()
2334                    .start_releasing()
2335                    .released()
2336                    .start_running()
2337                    .into_any();
2338
2339                let output = format!("{any_env}");
2340                assert_eq!(output, "Environment 'test-env' is in state: running");
2341            }
2342
2343            #[test]
2344            fn it_should_format_error_state_with_error_details() {
2345                let error_message = "network timeout";
2346                let any_env = super::create_test_environment_created()
2347                    .start_provisioning()
2348                    .provision_failed(super::create_test_provision_context(error_message))
2349                    .into_any();
2350
2351                let output = format!("{any_env}");
2352                assert_eq!(
2353                    output,
2354                    format!("Environment 'test-env' is in state: provision_failed (failed at: {error_message})")
2355                );
2356            }
2357
2358            #[test]
2359            fn it_should_format_configure_failed_with_error_details() {
2360                let error_message = "ansible error";
2361                let any_env = super::create_test_environment_created()
2362                    .start_provisioning()
2363                    .provisioned(
2364                        IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
2365                        ProvisionMethod::Provisioned,
2366                    )
2367                    .start_configuring()
2368                    .configure_failed(super::create_test_configure_context(error_message))
2369                    .into_any();
2370
2371                let output = format!("{any_env}");
2372                assert_eq!(
2373                    output,
2374                    format!("Environment 'test-env' is in state: configure_failed (failed at: {error_message})")
2375                );
2376            }
2377
2378            #[test]
2379            fn it_should_format_destroyed_state() {
2380                let any_env = super::create_test_environment_created()
2381                    .destroy()
2382                    .into_any();
2383
2384                let output = format!("{any_env}");
2385                assert_eq!(output, "Environment 'test-env' is in state: destroyed");
2386            }
2387
2388            #[test]
2389            fn it_should_work_with_println_macro() {
2390                let any_env = super::create_test_environment_created().into_any();
2391
2392                // This test verifies that Display can be used with println!
2393                // We can't capture println output easily, but we can verify it compiles
2394                let output = format!("{any_env}");
2395                assert!(output.contains("test-env"));
2396                assert!(output.contains("created"));
2397            }
2398        }
2399    }
2400}