Skip to main content

fakecloud_cloudformation/
resource_provisioner.rs

1use chrono::Utc;
2use parking_lot::RwLock;
3use std::collections::BTreeMap;
4use std::sync::Arc;
5use uuid::Uuid;
6
7use crate::state::SharedCloudFormationState;
8use fakecloud_acm::{
9    CertificateOptions as AcmCertificateOptions, DomainValidation as AcmDomainValidation,
10    RenewalSummary as AcmRenewalSummary, SharedAcmState, StoredCertificate as AcmStoredCertificate,
11};
12use fakecloud_apigateway::{
13    make_id as apigw_make_id, ApiKey as ApiGwApiKey, Authorizer as ApiGwAuthorizer,
14    Deployment as ApiGwDeployment, Integration as ApiGwIntegration, Method as ApiGwMethod,
15    Model as ApiGwModel, Resource as ApiGwResource, RestApi as ApiGwRestApi, SharedApiGatewayState,
16    Stage as ApiGwStage, UsagePlan as ApiGwUsagePlan,
17};
18use fakecloud_apigatewayv2::{
19    Authorizer as ApiGwV2Authorizer, CorsConfiguration as ApiGwV2CorsConfiguration,
20    Deployment as ApiGwV2Deployment, HttpApi as ApiGwV2HttpApi, Integration as ApiGwV2Integration,
21    JwtConfiguration as ApiGwV2JwtConfiguration, Route as ApiGwV2Route, SharedApiGatewayV2State,
22    Stage as ApiGwV2Stage,
23};
24use fakecloud_application_autoscaling::{
25    ScalableTarget as AppasScalableTarget, ScalingPolicy as AppasScalingPolicy,
26    SharedApplicationAutoScalingState as AppasState, SuspendedState as AppasSuspendedState,
27};
28use fakecloud_athena::{DataCatalog, NamedQuery, PreparedStatement, SharedAthenaState, WorkGroup};
29use fakecloud_cloudfront::{
30    functions::{
31        CloudFrontOriginAccessIdentityConfig, FunctionConfig, KeyGroupConfig, KeyGroupItems,
32        PublicKeyConfig, StoredFunction, StoredKeyGroup, StoredOriginAccessIdentity,
33        StoredPublicKey,
34    },
35    model::{
36        DefaultCacheBehavior, DistributionConfig, Origin, OriginItems, Origins, ViewerCertificate,
37    },
38    policies::{
39        CachePolicyConfig, OriginAccessControlConfig, OriginRequestPolicyConfig,
40        OriginRequestPolicyCookiesConfig, OriginRequestPolicyHeadersConfig,
41        OriginRequestPolicyQueryStringsConfig, ResponseHeadersPolicyConfig, StoredCachePolicy,
42        StoredOriginAccessControl, StoredOriginRequestPolicy, StoredResponseHeadersPolicy,
43    },
44    state::StoredDistribution,
45    SharedCloudFrontState,
46};
47use fakecloud_cloudwatch::{AlarmState, Dashboard, MetricAlarm, SharedCloudWatchState};
48use fakecloud_cognito::{
49    default_schema_attributes, AccountRecoverySetting, AdminCreateUserConfig,
50    CognitoIdentityProvider, CustomDomainConfig, EmailConfiguration, IdentityPool,
51    IdentityPoolRoleAttachment, PasswordPolicy, PoolPolicies, RecoveryOption, SchemaAttribute,
52    SharedCognitoState, SignInPolicy, SmsConfiguration, UserPool, UserPoolClient, UserPoolDomain,
53};
54use fakecloud_core::delivery::DeliveryBus;
55use fakecloud_dynamodb::{
56    AttributeDefinition, DynamoTable, KeySchemaElement, OnDemandThroughput, ProvisionedThroughput,
57    SharedDynamoDbState,
58};
59use fakecloud_ecr::{Repository, SharedEcrState};
60use fakecloud_ecs::{
61    CapacityProvider as EcsCapacityProvider, Cluster as EcsCluster, Service as EcsService,
62    SharedEcsState, TagEntry as EcsTagEntry, TaskDefinition as EcsTaskDefinition,
63};
64use fakecloud_elasticache::{
65    CacheCluster as EcCacheCluster, CacheParameterGroup, CacheSecurityGroup, CacheSubnetGroup,
66    ElastiCacheUser as EcUser, ElastiCacheUserGroup as EcUserGroup,
67    ReplicationGroup as EcReplicationGroup, SharedElastiCacheState,
68};
69use fakecloud_elbv2::{
70    Action as ElbAction, Listener, LoadBalancer, Rule as ElbRule, RuleCondition, SharedElbv2State,
71    Tag as ElbTag, TargetGroup, TargetGroupTuple,
72};
73use fakecloud_eventbridge::{
74    ApiDestination, Archive, Connection, Endpoint, EventBus, EventRule, SharedEventBridgeState,
75};
76use fakecloud_firehose::state::{DeliveryStream, S3Destination};
77use fakecloud_iam::{
78    IamAccessKey, IamGroup, IamInstanceProfile, IamPolicy, IamRole, IamUser, OidcProvider,
79    PolicyVersion, SamlProvider, SharedIamState, Tag, VirtualMfaDevice,
80};
81use fakecloud_kinesis::{build_stream_shards, KinesisConsumer, KinesisStream, SharedKinesisState};
82use fakecloud_kms::provisioner as kms_provisioner;
83use fakecloud_kms::SharedKmsState;
84use fakecloud_lambda::{
85    AttachedLayer, EventSourceMapping, FunctionAlias, FunctionUrlConfig, Layer, LayerVersion,
86    SharedLambdaState,
87};
88use fakecloud_logs::{
89    Delivery, DeliveryDestination, DeliverySource, Destination, LogStream, MetricFilter,
90    MetricTransformation, QueryDefinition, ResourcePolicy, SharedLogsState, SubscriptionFilter,
91};
92use fakecloud_organizations::{
93    OrganizationState, OrganizationalUnit, Policy as OrgPolicy, SharedOrganizationsState,
94    POLICY_TYPE_SCP,
95};
96use fakecloud_rds::{DbInstance, DbParameterGroup, DbSubnetGroup, RdsTag, SharedRdsState};
97use fakecloud_route53::{
98    model::{HealthCheckConfig, HostedZoneFeatures, ResourceRecordSet},
99    SharedRoute53State, StoredHealthCheck, StoredHostedZone,
100};
101use fakecloud_s3::{S3Bucket, SharedS3State};
102use fakecloud_secretsmanager::{RotationRules, Secret, SecretVersion, SharedSecretsManagerState};
103use fakecloud_ses::{
104    ConfigurationSet as SesConfigurationSet, ContactList as SesContactList,
105    DedicatedIpPool as SesDedicatedIpPool, EmailIdentity as SesEmailIdentity,
106    EmailTemplate as SesEmailTemplate, EventDestination as SesEventDestination,
107    IpFilter as SesIpFilter, ReceiptAction as SesReceiptAction, ReceiptFilter as SesReceiptFilter,
108    ReceiptRule as SesReceiptRule, ReceiptRuleSet as SesReceiptRuleSet, SharedSesState,
109};
110use fakecloud_sns::{SharedSnsState, SnsSubscription, SnsTopic};
111use fakecloud_sqs::{SharedSqsState, SqsQueue};
112use fakecloud_ssm::{SharedSsmState, SsmParameter};
113use fakecloud_stepfunctions::{
114    Activity as SfnActivity, AliasRoute, SharedStepFunctionsState, StateMachine, StateMachineAlias,
115    StateMachineStatus, StateMachineType, StateMachineVersion,
116};
117use fakecloud_wafv2::{IpSet, RegexPatternSet, RuleGroup, SharedWafv2State, WebAcl};
118
119use crate::state::StackResource;
120use crate::template::ResourceDefinition;
121
122/// Convert a CFN `Tags` property (`[{Key, Value}, ...]`) into the IAM
123/// crate's `Tag` Vec form. Silently skips malformed entries — the same
124/// tolerant behaviour the existing IAM service uses for runtime input.
125fn parse_iam_tags(value: Option<&serde_json::Value>) -> Vec<Tag> {
126    let Some(arr) = value.and_then(|v| v.as_array()) else {
127        return Vec::new();
128    };
129    arr.iter()
130        .filter_map(|t| {
131            let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
132            let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
133            Some(Tag { key, value })
134        })
135        .collect()
136}
137
138/// Mirror of `parse_iam_tags` but for the ELBv2 crate's separate `Tag`
139/// type. Same `[{Key, Value}, ...]` JSON shape, ignored on malformed entries.
140fn parse_elb_tags(value: Option<&serde_json::Value>) -> Vec<ElbTag> {
141    let Some(arr) = value.and_then(|v| v.as_array()) else {
142        return Vec::new();
143    };
144    arr.iter()
145        .filter_map(|t| {
146            let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
147            let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
148            Some(ElbTag { key, value })
149        })
150        .collect()
151}
152
153/// Translate CFN-shape Listener/ListenerRule actions into ELBv2 internal
154/// `Action`s. Only the action-type knobs CFN exposes are wired; anything
155/// not recognised becomes a bare action with no target.
156fn parse_elb_actions(value: Option<&serde_json::Value>) -> Vec<ElbAction> {
157    let Some(arr) = value.and_then(|v| v.as_array()) else {
158        return Vec::new();
159    };
160    arr.iter()
161        .map(|a| {
162            let action_type = a
163                .get("Type")
164                .and_then(|v| v.as_str())
165                .unwrap_or("forward")
166                .to_string();
167            let target_group_arn = a
168                .get("TargetGroupArn")
169                .and_then(|v| v.as_str())
170                .map(|s| s.to_string());
171            let order = a.get("Order").and_then(|v| v.as_i64()).map(|n| n as i32);
172            let redirect = a
173                .get("RedirectConfig")
174                .map(|r| fakecloud_elbv2::RedirectConfig {
175                    protocol: r
176                        .get("Protocol")
177                        .and_then(|v| v.as_str())
178                        .map(|s| s.to_string()),
179                    port: r
180                        .get("Port")
181                        .and_then(|v| v.as_str())
182                        .map(|s| s.to_string()),
183                    host: r
184                        .get("Host")
185                        .and_then(|v| v.as_str())
186                        .map(|s| s.to_string()),
187                    path: r
188                        .get("Path")
189                        .and_then(|v| v.as_str())
190                        .map(|s| s.to_string()),
191                    query: r
192                        .get("Query")
193                        .and_then(|v| v.as_str())
194                        .map(|s| s.to_string()),
195                    status_code: r
196                        .get("StatusCode")
197                        .and_then(|v| v.as_str())
198                        .unwrap_or("HTTP_302")
199                        .to_string(),
200                });
201            let fixed_response =
202                a.get("FixedResponseConfig")
203                    .map(|f| fakecloud_elbv2::FixedResponseConfig {
204                        message_body: f
205                            .get("MessageBody")
206                            .and_then(|v| v.as_str())
207                            .map(|s| s.to_string()),
208                        status_code: f
209                            .get("StatusCode")
210                            .and_then(|v| v.as_str())
211                            .unwrap_or("200")
212                            .to_string(),
213                        content_type: f
214                            .get("ContentType")
215                            .and_then(|v| v.as_str())
216                            .map(|s| s.to_string()),
217                    });
218            let forward = a.get("ForwardConfig").map(|f| {
219                let target_groups: Vec<TargetGroupTuple> = f
220                    .get("TargetGroups")
221                    .and_then(|v| v.as_array())
222                    .map(|arr| {
223                        arr.iter()
224                            .filter_map(|t| {
225                                let target_group_arn = t
226                                    .get("TargetGroupArn")
227                                    .and_then(|v| v.as_str())?
228                                    .to_string();
229                                let weight =
230                                    t.get("Weight").and_then(|v| v.as_i64()).map(|n| n as i32);
231                                Some(TargetGroupTuple {
232                                    target_group_arn,
233                                    weight,
234                                })
235                            })
236                            .collect()
237                    })
238                    .unwrap_or_default();
239                fakecloud_elbv2::ForwardConfig {
240                    target_groups,
241                    stickiness: None,
242                }
243            });
244            ElbAction {
245                action_type,
246                target_group_arn,
247                order,
248                redirect,
249                fixed_response,
250                forward,
251                authenticate_cognito: None,
252                authenticate_oidc: None,
253            }
254        })
255        .collect()
256}
257
258fn parse_elb_rule_conditions(value: Option<&serde_json::Value>) -> Vec<RuleCondition> {
259    let Some(arr) = value.and_then(|v| v.as_array()) else {
260        return Vec::new();
261    };
262    arr.iter()
263        .map(|c| {
264            let field = c
265                .get("Field")
266                .and_then(|v| v.as_str())
267                .unwrap_or("")
268                .to_string();
269            let values: Vec<String> = c
270                .get("Values")
271                .and_then(|v| v.as_array())
272                .map(|arr| {
273                    arr.iter()
274                        .filter_map(|s| s.as_str().map(|s| s.to_string()))
275                        .collect()
276                })
277                .unwrap_or_default();
278            let host_header_values: Vec<String> = c
279                .get("HostHeaderConfig")
280                .and_then(|v| v.get("Values"))
281                .and_then(|v| v.as_array())
282                .map(|arr| {
283                    arr.iter()
284                        .filter_map(|s| s.as_str().map(|s| s.to_string()))
285                        .collect()
286                })
287                .unwrap_or_default();
288            RuleCondition {
289                field,
290                values,
291                host_header_values,
292                path_pattern_values: Vec::new(),
293                http_header_name: None,
294                http_header_values: Vec::new(),
295                query_string_values: Vec::new(),
296                http_request_method_values: Vec::new(),
297                source_ip_values: Vec::new(),
298            }
299        })
300        .collect()
301}
302
303/// Parse the `KeyPolicy` field of an `AWS::KMS::Key` /
304/// `AWS::KMS::ReplicaKey` resource. CFN allows either a JSON string or
305/// an inline JSON object; we serialize the object form back to a string
306/// to match the [`KmsKey.policy`](fakecloud_kms::KmsKey) shape.
307fn parse_key_policy(props: &serde_json::Value) -> Option<String> {
308    match props.get("KeyPolicy") {
309        Some(v) if v.is_string() => Some(v.as_str().unwrap_or("").to_string()),
310        Some(v) => Some(serde_json::to_string(v).unwrap_or_default()),
311        None => None,
312    }
313}
314
315/// Parse the `Tags` field common to KMS CFN resources: an array of
316/// `{Key, Value}` pairs into a sorted map.
317fn parse_tag_list(props: &serde_json::Value) -> BTreeMap<String, String> {
318    let mut tags: BTreeMap<String, String> = BTreeMap::new();
319    if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
320        for t in arr {
321            if let (Some(k), Some(v)) = (
322                t.get("Key").and_then(|x| x.as_str()),
323                t.get("Value").and_then(|x| x.as_str()),
324            ) {
325                tags.insert(k.to_string(), v.to_string());
326            }
327        }
328    }
329    tags
330}
331
332/// Parse the `Properties` of an `AWS::KMS::Key` resource into the
333/// shared [`fakecloud_kms::provisioner::KeyCreationInput`]. Defaults
334/// match AWS: `ENCRYPT_DECRYPT` / `SYMMETRIC_DEFAULT` / `AWS_KMS` /
335/// `Enabled=true`. `RotationPeriodInDays`, `PendingWindowInDays`, and
336/// `BypassPolicyLockoutSafetyCheck` are accepted by the parser for
337/// CFN compatibility but not persisted on the key — the underlying
338/// KMS state doesn't model per-key rotation periods, and the deletion
339/// window only matters at scheduled-deletion time which CFN delete
340/// short-circuits.
341fn parse_kms_key_input(props: &serde_json::Value) -> kms_provisioner::KeyCreationInput {
342    kms_provisioner::KeyCreationInput {
343        description: props
344            .get("Description")
345            .and_then(|v| v.as_str())
346            .unwrap_or("")
347            .to_string(),
348        key_usage: props
349            .get("KeyUsage")
350            .and_then(|v| v.as_str())
351            .unwrap_or("ENCRYPT_DECRYPT")
352            .to_string(),
353        key_spec: props
354            .get("KeySpec")
355            .and_then(|v| v.as_str())
356            .unwrap_or("SYMMETRIC_DEFAULT")
357            .to_string(),
358        origin: props
359            .get("Origin")
360            .and_then(|v| v.as_str())
361            .unwrap_or("AWS_KMS")
362            .to_string(),
363        enabled: props
364            .get("Enabled")
365            .and_then(|v| v.as_bool())
366            .unwrap_or(true),
367        multi_region: props
368            .get("MultiRegion")
369            .and_then(|v| v.as_bool())
370            .unwrap_or(false),
371        key_rotation_enabled: props
372            .get("EnableKeyRotation")
373            .and_then(|v| v.as_bool())
374            .unwrap_or(false),
375        policy: parse_key_policy(props),
376        tags: parse_tag_list(props),
377    }
378}
379
380/// `LogGroupName` properties on Logs CFN resources may carry either a
381/// log-group ARN (when they come from `{Ref: SomeLogGroup}` in the same
382/// template) or a plain name. Extract the name in either case.
383fn parse_log_group_name(input: &str) -> String {
384    if let Some(rest) = input.strip_prefix("arn:aws:logs:") {
385        if let Some(after) = rest.split(":log-group:").nth(1) {
386            // ARN ends with `:*`; trim it if present.
387            return after.trim_end_matches(":*").to_string();
388        }
389    }
390    input.to_string()
391}
392
393/// Pull the function name out of either a bare name or a Lambda
394/// function ARN. CFN passes `{Ref: SomeFunction}` which resolves to the
395/// function name today, but `{Fn::GetAtt: [F, Arn]}` resolves to the
396/// full ARN; both shapes need to land at the same map key.
397fn parse_lambda_function_name(input: &str) -> String {
398    if let Some(rest) = input.strip_prefix("arn:aws:lambda:") {
399        if let Some(after) = rest.split(":function:").nth(1) {
400            // Trim trailing `:qualifier` (alias / version).
401            return after.split(':').next().unwrap_or(after).to_string();
402        }
403    }
404    input.to_string()
405}
406
407/// All AWS::Lambda::Function CFN properties parsed and pre-defaulted into
408/// their lambda-state shapes. Mirrors the lambda service's
409/// `CreateFunctionInput` so create + update share one parse path.
410struct LambdaFunctionProps {
411    runtime: String,
412    role: String,
413    handler: String,
414    description: String,
415    timeout: i64,
416    memory_size: i64,
417    package_type: String,
418    tags: BTreeMap<String, String>,
419    environment: BTreeMap<String, String>,
420    architectures: Vec<String>,
421    /// Decoded `Code.ZipFile` bytes (base64 → raw). `None` when the
422    /// caller specified `Code.S3Bucket`/`Code.S3Key` or `Code.ImageUri`
423    /// instead — the create/update path resolves S3 separately.
424    code_zip: Option<Vec<u8>>,
425    s3_bucket: Option<String>,
426    s3_key: Option<String>,
427    image_uri: Option<String>,
428    layers: Vec<String>,
429    tracing_mode: Option<String>,
430    kms_key_arn: Option<String>,
431    ephemeral_storage_size: Option<i64>,
432    vpc_config: Option<serde_json::Value>,
433    snap_start: Option<serde_json::Value>,
434    dead_letter_config_arn: Option<String>,
435    file_system_configs: Vec<serde_json::Value>,
436    logging_config: Option<serde_json::Value>,
437}
438
439/// Parse the `Properties` value of an `AWS::Lambda::Function` resource
440/// into a `LambdaFunctionProps`. Defaults match the AWS Lambda
441/// CreateFunction API: `python3.12` runtime, `index.handler` handler,
442/// `Zip` package type, `x86_64` architecture, 3s timeout, 128MB memory.
443fn parse_lambda_function_props(props: &serde_json::Value) -> Result<LambdaFunctionProps, String> {
444    let runtime = props
445        .get("Runtime")
446        .and_then(|v| v.as_str())
447        .unwrap_or("python3.12")
448        .to_string();
449    let role = props
450        .get("Role")
451        .and_then(|v| v.as_str())
452        .unwrap_or_default()
453        .to_string();
454    let handler = props
455        .get("Handler")
456        .and_then(|v| v.as_str())
457        .unwrap_or("index.handler")
458        .to_string();
459    let description = props
460        .get("Description")
461        .and_then(|v| v.as_str())
462        .unwrap_or_default()
463        .to_string();
464    let timeout = props.get("Timeout").and_then(|v| v.as_i64()).unwrap_or(3);
465    let memory_size = props
466        .get("MemorySize")
467        .and_then(|v| v.as_i64())
468        .unwrap_or(128);
469    let architectures = props
470        .get("Architectures")
471        .and_then(|v| v.as_array())
472        .map(|a| {
473            a.iter()
474                .filter_map(|v| v.as_str().map(|s| s.to_string()))
475                .collect::<Vec<_>>()
476        })
477        .unwrap_or_else(|| vec!["x86_64".to_string()]);
478    let package_type = props
479        .get("PackageType")
480        .and_then(|v| v.as_str())
481        .unwrap_or("Zip")
482        .to_string();
483    let environment = props
484        .get("Environment")
485        .and_then(|v| v.get("Variables"))
486        .and_then(|v| v.as_object())
487        .map(|o| {
488            o.iter()
489                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
490                .collect::<BTreeMap<String, String>>()
491        })
492        .unwrap_or_default();
493
494    // CFN tags ride as `[{Key, Value}, ...]`; flatten to the map shape
495    // the lambda crate stores tags in.
496    let tags: BTreeMap<String, String> = props
497        .get("Tags")
498        .and_then(|v| v.as_array())
499        .map(|arr| {
500            arr.iter()
501                .filter_map(|t| {
502                    let k = t.get("Key").and_then(|v| v.as_str())?.to_string();
503                    let v = t.get("Value").and_then(|v| v.as_str())?.to_string();
504                    Some((k, v))
505                })
506                .collect()
507        })
508        .unwrap_or_default();
509
510    let code = props.get("Code");
511    // CFN's `Code.ZipFile` is the raw source code inline (per the
512    // CloudFormation user guide), not base64 like the Lambda
513    // CreateFunction API. We store it as the raw bytes — fakecloud's
514    // Lambda runtime is content-agnostic and just needs *some* deployable
515    // payload to execute.
516    let code_zip = code
517        .and_then(|c| c.get("ZipFile"))
518        .and_then(|v| v.as_str())
519        .map(|s| s.as_bytes().to_vec());
520    let s3_bucket = code
521        .and_then(|c| c.get("S3Bucket"))
522        .and_then(|v| v.as_str())
523        .map(|s| s.to_string());
524    let s3_key = code
525        .and_then(|c| c.get("S3Key"))
526        .and_then(|v| v.as_str())
527        .map(|s| s.to_string());
528    // ImageUri is only meaningful for `PackageType=Image`. Mirroring the
529    // lambda service path, drop it on Zip functions so GetFunction
530    // doesn't return ECR metadata for a ZIP-based function.
531    let image_uri = if package_type == "Image" {
532        code.and_then(|c| c.get("ImageUri"))
533            .and_then(|v| v.as_str())
534            .map(|s| s.to_string())
535    } else {
536        None
537    };
538    if package_type == "Image" && image_uri.is_none() {
539        return Err("Code.ImageUri is required when PackageType is Image".to_string());
540    }
541
542    let layers: Vec<String> = props
543        .get("Layers")
544        .and_then(|v| v.as_array())
545        .map(|arr| {
546            arr.iter()
547                .filter_map(|v| v.as_str().map(String::from))
548                .collect()
549        })
550        .unwrap_or_default();
551
552    let tracing_mode = props
553        .get("TracingConfig")
554        .and_then(|v| v.get("Mode"))
555        .and_then(|v| v.as_str())
556        .map(String::from);
557    let kms_key_arn = props
558        .get("KmsKeyArn")
559        .and_then(|v| v.as_str())
560        .map(String::from);
561    let ephemeral_storage_size = props
562        .get("EphemeralStorage")
563        .and_then(|v| v.get("Size"))
564        .and_then(|v| v.as_i64());
565    let vpc_config = props.get("VpcConfig").filter(|v| v.is_object()).cloned();
566    let snap_start = props.get("SnapStart").filter(|v| v.is_object()).cloned();
567    let dead_letter_config_arn = props
568        .get("DeadLetterConfig")
569        .and_then(|v| v.get("TargetArn"))
570        .and_then(|v| v.as_str())
571        .map(String::from);
572    let file_system_configs = props
573        .get("FileSystemConfigs")
574        .and_then(|v| v.as_array())
575        .cloned()
576        .unwrap_or_default();
577    let logging_config = props
578        .get("LoggingConfig")
579        .filter(|v| v.is_object())
580        .cloned();
581
582    Ok(LambdaFunctionProps {
583        runtime,
584        role,
585        handler,
586        description,
587        timeout,
588        memory_size,
589        package_type,
590        tags,
591        environment,
592        architectures,
593        code_zip,
594        s3_bucket,
595        s3_key,
596        image_uri,
597        layers,
598        tracing_mode,
599        kms_key_arn,
600        ephemeral_storage_size,
601        vpc_config,
602        snap_start,
603        dead_letter_config_arn,
604        file_system_configs,
605        logging_config,
606    })
607}
608
609/// Properties for `AWS::Lambda::EventSourceMapping` shared between the
610/// create and update provisioners. Mirrors the lambda crate
611/// `EventSourceMapping` shape one-for-one — every CFN-mutable field
612/// lands here so update can rewrite without re-parsing.
613struct LambdaEventSourceMappingProps {
614    event_source_arn: String,
615    batch_size: i64,
616    enabled: bool,
617    starting_position: Option<String>,
618    starting_position_timestamp: Option<f64>,
619    parallelization_factor: Option<i64>,
620    maximum_batching_window_in_seconds: Option<i64>,
621    function_response_types: Vec<String>,
622    filter_patterns: Vec<String>,
623    kms_key_arn: Option<String>,
624    metrics_config: Option<serde_json::Value>,
625    destination_config: Option<serde_json::Value>,
626    maximum_retry_attempts: Option<i64>,
627    maximum_record_age_in_seconds: Option<i64>,
628    bisect_batch_on_function_error: Option<bool>,
629    tumbling_window_in_seconds: Option<i64>,
630    topics: Vec<String>,
631    queues: Vec<String>,
632}
633
634/// Parse the `Properties` value of an `AWS::Lambda::EventSourceMapping`
635/// resource. `EventSourceArn` is required on create; updates re-parse but
636/// the value is ignored since the field is immutable.
637fn parse_lambda_event_source_mapping_props(
638    props: &serde_json::Value,
639) -> Result<LambdaEventSourceMappingProps, String> {
640    let event_source_arn = props
641        .get("EventSourceArn")
642        .and_then(|v| v.as_str())
643        .unwrap_or_default()
644        .to_string();
645    let batch_size = props
646        .get("BatchSize")
647        .and_then(|v| v.as_i64())
648        .unwrap_or(10);
649    let enabled = props
650        .get("Enabled")
651        .and_then(|v| v.as_bool())
652        .unwrap_or(true);
653    let starting_position = props
654        .get("StartingPosition")
655        .and_then(|v| v.as_str())
656        .map(|s| s.to_string());
657    let starting_position_timestamp = props
658        .get("StartingPositionTimestamp")
659        .and_then(|v| v.as_f64());
660    let parallelization_factor = props.get("ParallelizationFactor").and_then(|v| v.as_i64());
661    let maximum_batching_window_in_seconds = props
662        .get("MaximumBatchingWindowInSeconds")
663        .and_then(|v| v.as_i64());
664    let function_response_types: Vec<String> = props
665        .get("FunctionResponseTypes")
666        .and_then(|v| v.as_array())
667        .map(|arr| {
668            arr.iter()
669                .filter_map(|v| v.as_str().map(|s| s.to_string()))
670                .collect()
671        })
672        .unwrap_or_default();
673    let filter_patterns: Vec<String> = props
674        .get("FilterCriteria")
675        .and_then(|v| v.get("Filters"))
676        .and_then(|v| v.as_array())
677        .map(|arr| {
678            arr.iter()
679                .filter_map(|f| {
680                    f.get("Pattern")
681                        .and_then(|p| p.as_str())
682                        .map(|s| s.to_string())
683                })
684                .collect()
685        })
686        .unwrap_or_default();
687    let kms_key_arn = props
688        .get("KmsKeyArn")
689        .and_then(|v| v.as_str())
690        .map(|s| s.to_string());
691    let metrics_config = props
692        .get("MetricsConfig")
693        .filter(|v| v.is_object())
694        .cloned();
695    let destination_config = props
696        .get("DestinationConfig")
697        .filter(|v| v.is_object())
698        .cloned();
699    let maximum_retry_attempts = props.get("MaximumRetryAttempts").and_then(|v| v.as_i64());
700    let maximum_record_age_in_seconds = props
701        .get("MaximumRecordAgeInSeconds")
702        .and_then(|v| v.as_i64());
703    let bisect_batch_on_function_error = props
704        .get("BisectBatchOnFunctionError")
705        .and_then(|v| v.as_bool());
706    let tumbling_window_in_seconds = props
707        .get("TumblingWindowInSeconds")
708        .and_then(|v| v.as_i64());
709    let topics: Vec<String> = props
710        .get("Topics")
711        .and_then(|v| v.as_array())
712        .map(|arr| {
713            arr.iter()
714                .filter_map(|v| v.as_str().map(|s| s.to_string()))
715                .collect()
716        })
717        .unwrap_or_default();
718    let queues: Vec<String> = props
719        .get("Queues")
720        .and_then(|v| v.as_array())
721        .map(|arr| {
722            arr.iter()
723                .filter_map(|v| v.as_str().map(|s| s.to_string()))
724                .collect()
725        })
726        .unwrap_or_default();
727
728    Ok(LambdaEventSourceMappingProps {
729        event_source_arn,
730        batch_size,
731        enabled,
732        starting_position,
733        starting_position_timestamp,
734        parallelization_factor,
735        maximum_batching_window_in_seconds,
736        function_response_types,
737        filter_patterns,
738        kms_key_arn,
739        metrics_config,
740        destination_config,
741        maximum_retry_attempts,
742        maximum_record_age_in_seconds,
743        bisect_batch_on_function_error,
744        tumbling_window_in_seconds,
745        topics,
746        queues,
747    })
748}
749
750/// Compute base64-encoded SHA-256 of code bytes — matches what the
751/// lambda service stores in `LambdaFunction.code_sha256`.
752fn sha256_b64(bytes: &[u8]) -> String {
753    use sha2::Digest;
754    let hash = sha2::Sha256::digest(bytes);
755    base64::Engine::encode(&base64::engine::general_purpose::STANDARD, hash)
756}
757
758/// Look up the `code_size` for an attached layer-version ARN by walking
759/// the in-process lambda state. Falls back to 0 when the ARN is
760/// unparseable or the layer/version is unknown — same fallback as the
761/// lambda service helper.
762fn layer_code_size(
763    accounts: &fakecloud_core::multi_account::MultiAccountState<fakecloud_lambda::LambdaState>,
764    arn: &str,
765) -> i64 {
766    // arn:aws:lambda:<region>:<account>:layer:<name>:<version>
767    let Some(rest) = arn.strip_prefix("arn:aws:lambda:") else {
768        return 0;
769    };
770    let mut parts = rest.split(':');
771    let _region = parts.next();
772    let Some(account) = parts.next() else {
773        return 0;
774    };
775    if parts.next() != Some("layer") {
776        return 0;
777    }
778    let Some(name) = parts.next() else {
779        return 0;
780    };
781    let Some(ver_str) = parts.next() else {
782        return 0;
783    };
784    let Ok(ver) = ver_str.parse::<i64>() else {
785        return 0;
786    };
787    accounts
788        .get(account)
789        .and_then(|s| s.layers.get(name))
790        .and_then(|l| l.versions.iter().find(|v| v.version == ver))
791        .map(|v| v.code_size)
792        .unwrap_or(0)
793}
794
795/// What a resource provisioner returns. The physical id is what `Ref` resolves
796/// to; `attributes` is what `Fn::GetAtt` resolves to (per-resource-type).
797pub struct ProvisionResult {
798    pub physical_id: String,
799    pub attributes: BTreeMap<String, String>,
800}
801
802impl ProvisionResult {
803    pub fn new(physical_id: impl Into<String>) -> Self {
804        Self {
805            physical_id: physical_id.into(),
806            attributes: BTreeMap::new(),
807        }
808    }
809
810    pub fn with(mut self, key: &str, value: impl Into<String>) -> Self {
811        self.attributes.insert(key.to_string(), value.into());
812        self
813    }
814
815    /// Merge another attribute map into this result. Used by update
816    /// handlers that delegate to a create handler but need to keep the
817    /// existing physical id while inheriting the freshly computed
818    /// attributes.
819    pub fn merge_attributes(mut self, other: BTreeMap<String, String>) -> Self {
820        for (k, v) in other {
821            self.attributes.insert(k, v);
822        }
823        self
824    }
825}
826
827/// Holds references to all service states so CloudFormation can provision resources.
828pub struct ResourceProvisioner {
829    pub sqs_state: SharedSqsState,
830    pub sns_state: SharedSnsState,
831    pub ssm_state: SharedSsmState,
832    pub iam_state: SharedIamState,
833    pub s3_state: SharedS3State,
834    pub eventbridge_state: SharedEventBridgeState,
835    pub dynamodb_state: SharedDynamoDbState,
836    pub logs_state: SharedLogsState,
837    pub lambda_state: SharedLambdaState,
838    pub secretsmanager_state: SharedSecretsManagerState,
839    pub kinesis_state: SharedKinesisState,
840    pub kms_state: SharedKmsState,
841    pub ecr_state: SharedEcrState,
842    pub cloudwatch_state: SharedCloudWatchState,
843    pub elbv2_state: SharedElbv2State,
844    pub organizations_state: SharedOrganizationsState,
845    pub cognito_state: SharedCognitoState,
846    pub rds_state: SharedRdsState,
847    pub ecs_state: SharedEcsState,
848    pub acm_state: SharedAcmState,
849    pub elasticache_state: SharedElastiCacheState,
850    pub route53_state: SharedRoute53State,
851    pub cloudfront_state: SharedCloudFrontState,
852    pub stepfunctions_state: SharedStepFunctionsState,
853    pub wafv2_state: SharedWafv2State,
854    pub apigateway_state: SharedApiGatewayState,
855    pub apigatewayv2_state: SharedApiGatewayV2State,
856    pub ses_state: SharedSesState,
857    pub app_autoscaling_state: AppasState,
858    pub athena_state: SharedAthenaState,
859    pub firehose_state: fakecloud_firehose::SharedFirehoseState,
860    pub glue_state: fakecloud_glue::SharedGlueState,
861    pub cloudformation_state: SharedCloudFormationState,
862    pub delivery: Arc<DeliveryBus>,
863    pub account_id: String,
864    pub region: String,
865    pub stack_id: String,
866}
867
868impl ResourceProvisioner {
869    /// Create a resource and return the StackResource with physical ID.
870    pub fn create_resource(&self, resource: &ResourceDefinition) -> Result<StackResource, String> {
871        let result = match resource.resource_type.as_str() {
872            "AWS::SQS::Queue" => self.create_sqs_queue(resource),
873            "AWS::SNS::Topic" => self.create_sns_topic(resource),
874            "AWS::SNS::Subscription" => self.create_sns_subscription(resource),
875            "AWS::SSM::Parameter" => self.create_ssm_parameter(resource),
876            "AWS::IAM::Role" => self.create_iam_role(resource),
877            "AWS::IAM::Policy" => self.create_iam_policy(resource),
878            "AWS::IAM::User" => self.create_iam_user(resource),
879            "AWS::IAM::Group" => self.create_iam_group(resource),
880            "AWS::IAM::ManagedPolicy" => self.create_iam_managed_policy(resource),
881            "AWS::IAM::UserToGroupAddition" => self.create_iam_user_to_group_addition(resource),
882            "AWS::IAM::AccessKey" => self.create_iam_access_key(resource),
883            "AWS::IAM::InstanceProfile" => self.create_iam_instance_profile(resource),
884            "AWS::IAM::OIDCProvider" => self.create_iam_oidc_provider(resource),
885            "AWS::IAM::SAMLProvider" => self.create_iam_saml_provider(resource),
886            "AWS::IAM::ServiceLinkedRole" => self.create_iam_service_linked_role(resource),
887            "AWS::IAM::VirtualMFADevice" => self.create_iam_virtual_mfa_device(resource),
888            "AWS::S3::Bucket" => self.create_s3_bucket(resource),
889            "AWS::Events::Rule" => self.create_eventbridge_rule(resource),
890            "AWS::Events::Connection" => self.create_eventbridge_connection(resource),
891            "AWS::Events::ApiDestination" => self.create_eventbridge_api_destination(resource),
892            "AWS::Events::Archive" => self.create_eventbridge_archive(resource),
893            "AWS::Events::EventBus" => self.create_eventbridge_event_bus(resource),
894            "AWS::Events::EventBusPolicy" => self.create_eventbridge_event_bus_policy(resource),
895            "AWS::Events::Endpoint" => self.create_eventbridge_endpoint(resource),
896            "AWS::DynamoDB::Table" => self.create_dynamodb_table(resource),
897            "AWS::Logs::LogGroup" => self.create_log_group(resource),
898            "AWS::Logs::LogStream" => self.create_log_stream(resource),
899            "AWS::Logs::MetricFilter" => self.create_metric_filter(resource),
900            "AWS::Logs::SubscriptionFilter" => self.create_subscription_filter(resource),
901            "AWS::Logs::Destination" => self.create_logs_destination(resource),
902            "AWS::Logs::ResourcePolicy" => self.create_logs_resource_policy(resource),
903            "AWS::Logs::QueryDefinition" => self.create_logs_query_definition(resource),
904            "AWS::Logs::Delivery" => self.create_logs_delivery(resource),
905            "AWS::Logs::DeliveryDestination" => self.create_logs_delivery_destination(resource),
906            "AWS::Logs::DeliverySource" => self.create_logs_delivery_source(resource),
907            "AWS::Lambda::Function" => self.create_lambda_function(resource),
908            "AWS::Lambda::Permission" => self.create_lambda_permission(resource),
909            "AWS::Lambda::EventSourceMapping" => self.create_lambda_event_source_mapping(resource),
910            "AWS::Lambda::LayerVersion" => self.create_lambda_layer_version(resource),
911            "AWS::Lambda::Url" => self.create_lambda_url(resource),
912            "AWS::Lambda::Alias" => self.create_lambda_alias(resource),
913            "AWS::Lambda::Version" => self.create_lambda_version(resource),
914            "AWS::SecretsManager::Secret" => self.create_secrets_manager_secret(resource),
915            "AWS::Kinesis::Stream" => self.create_kinesis_stream(resource),
916            "AWS::Kinesis::StreamConsumer" => self.create_kinesis_stream_consumer(resource),
917            "AWS::KMS::Key" => self.create_kms_key(resource),
918            "AWS::KMS::Alias" => self.create_kms_alias(resource),
919            "AWS::KMS::ReplicaKey" => self.create_kms_replica_key(resource),
920            "AWS::ECR::Repository" => self.create_ecr_repository(resource),
921            "AWS::ECR::RepositoryPolicy" => self.create_ecr_repository_policy(resource),
922            "AWS::ECR::LifecyclePolicy" => self.create_ecr_lifecycle_policy(resource),
923            "AWS::ECR::RegistryPolicy" => self.create_ecr_registry_policy(resource),
924            "AWS::ECR::ReplicationConfiguration" => {
925                self.create_ecr_replication_configuration(resource)
926            }
927            "AWS::ECR::RegistryScanningConfiguration" => {
928                self.create_ecr_registry_scanning_configuration(resource)
929            }
930            "AWS::ECR::PullThroughCacheRule" => self.create_ecr_pull_through_cache_rule(resource),
931            "AWS::CloudWatch::Alarm" => self.create_cloudwatch_alarm(resource),
932            "AWS::CloudWatch::Dashboard" => self.create_cloudwatch_dashboard(resource),
933            "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
934                self.create_elbv2_load_balancer(resource)
935            }
936            "AWS::ElasticLoadBalancingV2::TargetGroup" => self.create_elbv2_target_group(resource),
937            "AWS::ElasticLoadBalancingV2::Listener" => self.create_elbv2_listener(resource),
938            "AWS::ElasticLoadBalancingV2::ListenerRule" => {
939                self.create_elbv2_listener_rule(resource)
940            }
941            "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
942                self.create_elbv2_listener_certificate(resource)
943            }
944            "AWS::ElasticLoadBalancingV2::TrustStore" => self.create_elbv2_trust_store(resource),
945            "AWS::Organizations::Organization" => self.create_organization(resource),
946            "AWS::Organizations::OrganizationalUnit" => self.create_organization_unit(resource),
947            "AWS::Organizations::Account" => self.create_organization_account(resource),
948            "AWS::Organizations::Policy" => self.create_organization_policy(resource),
949            "AWS::Organizations::ResourcePolicy" => {
950                self.create_organization_resource_policy(resource)
951            }
952            "AWS::Cognito::UserPool" => self.create_cognito_user_pool(resource),
953            "AWS::Cognito::UserPoolClient" => self.create_cognito_user_pool_client(resource),
954            "AWS::Cognito::UserPoolDomain" => self.create_cognito_user_pool_domain(resource),
955            "AWS::Cognito::IdentityPool" => self.create_cognito_identity_pool(resource),
956            "AWS::Cognito::IdentityPoolRoleAttachment" => {
957                self.create_cognito_identity_pool_role_attachment(resource)
958            }
959            "AWS::RDS::DBSubnetGroup" => self.create_rds_subnet_group(resource),
960            "AWS::RDS::DBParameterGroup" => self.create_rds_parameter_group(resource),
961            "AWS::RDS::DBClusterParameterGroup" => {
962                self.create_rds_cluster_parameter_group(resource)
963            }
964            "AWS::RDS::OptionGroup" => self.create_rds_option_group(resource),
965            "AWS::RDS::EventSubscription" => self.create_rds_event_subscription(resource),
966            "AWS::RDS::DBSecurityGroup" => self.create_rds_security_group(resource),
967            "AWS::RDS::DBProxy" => self.create_rds_db_proxy(resource),
968            "AWS::RDS::DBInstance" => self.create_rds_db_instance(resource),
969            "AWS::RDS::DBCluster" => self.create_rds_db_cluster(resource),
970            "AWS::ECS::Cluster" => self.create_ecs_cluster(resource),
971            "AWS::ECS::TaskDefinition" => self.create_ecs_task_definition(resource),
972            "AWS::ECS::Service" => self.create_ecs_service(resource),
973            "AWS::ECS::CapacityProvider" => self.create_ecs_capacity_provider(resource),
974            "AWS::CertificateManager::Certificate" => self.create_acm_certificate(resource),
975            "AWS::CertificateManager::Account" => self.create_acm_account(resource),
976            "AWS::ElastiCache::ParameterGroup" => self.create_ec_parameter_group(resource),
977            "AWS::ElastiCache::SubnetGroup" => self.create_ec_subnet_group(resource),
978            "AWS::ElastiCache::SecurityGroup" => self.create_ec_security_group(resource),
979            "AWS::ElastiCache::User" => self.create_ec_user(resource),
980            "AWS::ElastiCache::UserGroup" => self.create_ec_user_group(resource),
981            "AWS::ElastiCache::CacheCluster" => self.create_ec_cache_cluster(resource),
982            "AWS::ElastiCache::ReplicationGroup" => self.create_ec_replication_group(resource),
983            "AWS::Route53::HostedZone" => self.create_route53_hosted_zone(resource),
984            "AWS::Route53::RecordSet" => self.create_route53_record_set(resource),
985            "AWS::Route53::HealthCheck" => self.create_route53_health_check(resource),
986            "AWS::Route53::DNSSEC" => self.create_route53_dnssec(resource),
987            "AWS::Route53::KeySigningKey" => self.create_route53_key_signing_key(resource),
988            "AWS::CloudFront::CloudFrontOriginAccessIdentity" => {
989                self.create_cf_origin_access_identity(resource)
990            }
991            "AWS::CloudFront::Distribution" => self.create_cf_distribution(resource),
992            "AWS::CloudFront::OriginAccessControl" => {
993                self.create_cf_origin_access_control(resource)
994            }
995            "AWS::CloudFront::PublicKey" => self.create_cf_public_key(resource),
996            "AWS::CloudFront::KeyGroup" => self.create_cf_key_group(resource),
997            "AWS::CloudFront::Function" => self.create_cf_function(resource),
998            "AWS::CloudFront::CachePolicy" => self.create_cf_cache_policy(resource),
999            "AWS::CloudFront::OriginRequestPolicy" => {
1000                self.create_cf_origin_request_policy(resource)
1001            }
1002            "AWS::CloudFront::ResponseHeadersPolicy" => {
1003                self.create_cf_response_headers_policy(resource)
1004            }
1005            "AWS::StepFunctions::StateMachine" => self.create_sfn_state_machine(resource),
1006            "AWS::StepFunctions::Activity" => self.create_sfn_activity(resource),
1007            "AWS::StepFunctions::StateMachineVersion" => self.create_sfn_version(resource),
1008            "AWS::StepFunctions::StateMachineAlias" => self.create_sfn_alias(resource),
1009            "AWS::WAFv2::WebACL" => self.create_wafv2_web_acl(resource),
1010            "AWS::WAFv2::IPSet" => self.create_wafv2_ip_set(resource),
1011            "AWS::WAFv2::RegexPatternSet" => self.create_wafv2_regex_pattern_set(resource),
1012            "AWS::WAFv2::RuleGroup" => self.create_wafv2_rule_group(resource),
1013            "AWS::WAFv2::LoggingConfiguration" => self.create_wafv2_logging_configuration(resource),
1014            "AWS::WAFv2::WebACLAssociation" => self.create_wafv2_web_acl_association(resource),
1015            "AWS::ApiGateway::RestApi" => self.create_apigw_rest_api(resource),
1016            "AWS::ApiGateway::Resource" => self.create_apigw_resource(resource),
1017            "AWS::ApiGateway::Method" => self.create_apigw_method(resource),
1018            "AWS::ApiGateway::Deployment" => self.create_apigw_deployment(resource),
1019            "AWS::ApiGateway::Stage" => self.create_apigw_stage(resource),
1020            "AWS::ApiGateway::Authorizer" => self.create_apigw_authorizer(resource),
1021            "AWS::ApiGateway::RequestValidator" => self.create_apigw_request_validator(resource),
1022            "AWS::ApiGateway::Model" => self.create_apigw_model(resource),
1023            "AWS::ApiGateway::GatewayResponse" => self.create_apigw_gateway_response(resource),
1024            "AWS::ApiGateway::UsagePlan" => self.create_apigw_usage_plan(resource),
1025            "AWS::ApiGateway::ApiKey" => self.create_apigw_api_key(resource),
1026            "AWS::ApiGateway::UsagePlanKey" => self.create_apigw_usage_plan_key(resource),
1027            "AWS::ApiGateway::DomainName" => self.create_apigw_domain_name(resource),
1028            "AWS::ApiGateway::BasePathMapping" => self.create_apigw_base_path_mapping(resource),
1029            "AWS::ApiGatewayV2::Api" => self.create_apigwv2_api(resource),
1030            "AWS::ApiGatewayV2::Route" => self.create_apigwv2_route(resource),
1031            "AWS::ApiGatewayV2::Integration" => self.create_apigwv2_integration(resource),
1032            "AWS::ApiGatewayV2::IntegrationResponse" => {
1033                self.create_apigwv2_integration_response(resource)
1034            }
1035            "AWS::ApiGatewayV2::RouteResponse" => self.create_apigwv2_route_response(resource),
1036            "AWS::ApiGatewayV2::Stage" => self.create_apigwv2_stage(resource),
1037            "AWS::ApiGatewayV2::Deployment" => self.create_apigwv2_deployment(resource),
1038            "AWS::ApiGatewayV2::Authorizer" => self.create_apigwv2_authorizer(resource),
1039            "AWS::ApiGatewayV2::DomainName" => self.create_apigwv2_domain_name(resource),
1040            "AWS::ApiGatewayV2::ApiMapping" => self.create_apigwv2_api_mapping(resource),
1041            "AWS::ApiGatewayV2::VpcLink" => self.create_apigwv2_vpc_link(resource),
1042            "AWS::ApiGatewayV2::Model" => self.create_apigwv2_model(resource),
1043            "AWS::SES::ConfigurationSet" => self.create_ses_configuration_set(resource),
1044            "AWS::SES::ConfigurationSetEventDestination" => {
1045                self.create_ses_event_destination(resource)
1046            }
1047            "AWS::SES::EmailIdentity" => self.create_ses_email_identity(resource),
1048            "AWS::SES::Template" => self.create_ses_template(resource),
1049            "AWS::SES::ContactList" => self.create_ses_contact_list(resource),
1050            "AWS::SES::DedicatedIpPool" => self.create_ses_dedicated_ip_pool(resource),
1051            "AWS::SES::ReceiptRule" => self.create_ses_receipt_rule(resource),
1052            "AWS::SES::ReceiptRuleSet" => self.create_ses_receipt_rule_set(resource),
1053            "AWS::SES::ReceiptFilter" => self.create_ses_receipt_filter(resource),
1054            "AWS::SES::VdmAttributes" => self.create_ses_vdm_attributes(resource),
1055            "AWS::SecretsManager::RotationSchedule" => {
1056                self.create_secrets_manager_rotation_schedule(resource)
1057            }
1058            "AWS::SecretsManager::ResourcePolicy" => {
1059                self.create_secrets_manager_resource_policy(resource)
1060            }
1061            "AWS::SecretsManager::SecretTargetAttachment" => {
1062                self.create_secrets_manager_target_attachment(resource)
1063            }
1064            "AWS::ApplicationAutoScaling::ScalableTarget" => {
1065                self.create_application_autoscaling_scalable_target(resource)
1066            }
1067            "AWS::ApplicationAutoScaling::ScalingPolicy" => {
1068                self.create_application_autoscaling_scaling_policy(resource)
1069            }
1070            "AWS::Athena::DataCatalog" => self.create_athena_data_catalog(resource),
1071            "AWS::Athena::NamedQuery" => self.create_athena_named_query(resource),
1072            "AWS::Athena::WorkGroup" => self.create_athena_work_group(resource),
1073            "AWS::Athena::PreparedStatement" => self.create_athena_prepared_statement(resource),
1074            "AWS::KinesisFirehose::DeliveryStream" => {
1075                self.create_firehose_delivery_stream(resource)
1076            }
1077            "AWS::Glue::Database" => self.create_glue_database(resource),
1078            "AWS::CloudFormation::Stack" => self.create_cloudformation_stack(resource),
1079            "AWS::Glue::Table" => self.create_glue_table(resource),
1080            "AWS::Glue::Partition" => self.create_glue_partition(resource),
1081            t if t.starts_with("Custom::") || t == "AWS::CloudFormation::CustomResource" => self
1082                .create_custom_resource(resource)
1083                .map(ProvisionResult::new),
1084            other => Err(format!("Unsupported resource type: {other}")),
1085        };
1086
1087        let is_custom = resource.resource_type.starts_with("Custom::")
1088            || resource.resource_type == "AWS::CloudFormation::CustomResource";
1089        let service_token = if is_custom {
1090            resource
1091                .properties
1092                .get("ServiceToken")
1093                .and_then(|v| v.as_str())
1094                .map(|s| s.to_string())
1095        } else {
1096            None
1097        };
1098
1099        result.map(|res| StackResource {
1100            logical_id: resource.logical_id.clone(),
1101            physical_id: res.physical_id,
1102            resource_type: resource.resource_type.clone(),
1103            status: "CREATE_COMPLETE".to_string(),
1104            service_token,
1105            attributes: res.attributes,
1106        })
1107    }
1108
1109    /// Apply a property update to an existing stack resource. Returns
1110    /// `Ok(Some(updated))` when the resource type supports in-place updates
1111    /// (the caller swaps the resulting `StackResource` for the old one) or
1112    /// `Ok(None)` when the type has no update path defined (the caller
1113    /// leaves the existing resource alone). `Err` propagates a
1114    /// resource-level failure up to the stack-level UPDATE_FAILED status.
1115    pub fn update_resource(
1116        &self,
1117        existing: &StackResource,
1118        new_def: &ResourceDefinition,
1119    ) -> Result<Option<StackResource>, String> {
1120        let result = match new_def.resource_type.as_str() {
1121            "AWS::Lambda::Function" => Some(self.update_lambda_function(existing, new_def)?),
1122            "AWS::Lambda::Permission" => Some(self.update_lambda_permission(existing, new_def)?),
1123            "AWS::Lambda::EventSourceMapping" => {
1124                Some(self.update_lambda_event_source_mapping(existing, new_def)?)
1125            }
1126            "AWS::Lambda::LayerVersion" => {
1127                Some(self.update_lambda_layer_version(existing, new_def)?)
1128            }
1129            "AWS::Lambda::Url" => Some(self.update_lambda_url(existing, new_def)?),
1130            "AWS::Lambda::Alias" => Some(self.update_lambda_alias(existing, new_def)?),
1131            "AWS::Lambda::Version" => Some(self.update_lambda_version(existing, new_def)?),
1132            "AWS::ApiGateway::RestApi" => Some(self.update_apigw_rest_api(existing, new_def)?),
1133            "AWS::ApiGateway::Resource" => Some(self.update_apigw_resource(existing, new_def)?),
1134            "AWS::ApiGateway::Method" => Some(self.update_apigw_method(existing, new_def)?),
1135            "AWS::ApiGateway::Deployment" => Some(self.update_apigw_deployment(existing, new_def)?),
1136            "AWS::ApiGateway::Stage" => Some(self.update_apigw_stage(existing, new_def)?),
1137            "AWS::ApiGateway::Authorizer" => Some(self.update_apigw_authorizer(existing, new_def)?),
1138            "AWS::ApiGateway::RequestValidator" => {
1139                Some(self.update_apigw_request_validator(existing, new_def)?)
1140            }
1141            "AWS::ApiGateway::Model" => Some(self.update_apigw_model(existing, new_def)?),
1142            "AWS::ApiGateway::GatewayResponse" => {
1143                Some(self.update_apigw_gateway_response(existing, new_def)?)
1144            }
1145            "AWS::ApiGateway::UsagePlan" => Some(self.update_apigw_usage_plan(existing, new_def)?),
1146            "AWS::ApiGateway::ApiKey" => Some(self.update_apigw_api_key(existing, new_def)?),
1147            "AWS::ApiGateway::UsagePlanKey" => {
1148                Some(self.update_apigw_usage_plan_key(existing, new_def)?)
1149            }
1150            "AWS::ApiGateway::DomainName" => {
1151                Some(self.update_apigw_domain_name(existing, new_def)?)
1152            }
1153            "AWS::ApiGateway::BasePathMapping" => {
1154                Some(self.update_apigw_base_path_mapping(existing, new_def)?)
1155            }
1156            "AWS::ApiGatewayV2::Api" => Some(self.update_apigwv2_api(existing, new_def)?),
1157            "AWS::ApiGatewayV2::Route" => Some(self.update_apigwv2_route(existing, new_def)?),
1158            "AWS::ApiGatewayV2::Integration" => {
1159                Some(self.update_apigwv2_integration(existing, new_def)?)
1160            }
1161            "AWS::ApiGatewayV2::IntegrationResponse" => {
1162                Some(self.update_apigwv2_integration_response(existing, new_def)?)
1163            }
1164            "AWS::ApiGatewayV2::RouteResponse" => {
1165                Some(self.update_apigwv2_route_response(existing, new_def)?)
1166            }
1167            "AWS::ApiGatewayV2::Stage" => Some(self.update_apigwv2_stage(existing, new_def)?),
1168            "AWS::ApiGatewayV2::Deployment" => {
1169                Some(self.update_apigwv2_deployment(existing, new_def)?)
1170            }
1171            "AWS::ApiGatewayV2::Authorizer" => {
1172                Some(self.update_apigwv2_authorizer(existing, new_def)?)
1173            }
1174            "AWS::ApiGatewayV2::DomainName" => {
1175                Some(self.update_apigwv2_domain_name(existing, new_def)?)
1176            }
1177            "AWS::ApiGatewayV2::ApiMapping" => {
1178                Some(self.update_apigwv2_api_mapping(existing, new_def)?)
1179            }
1180            "AWS::ApiGatewayV2::VpcLink" => Some(self.update_apigwv2_vpc_link(existing, new_def)?),
1181            "AWS::ApiGatewayV2::Model" => Some(self.update_apigwv2_model(existing, new_def)?),
1182            "AWS::ECS::Cluster" => Some(self.update_ecs_cluster(existing, new_def)?),
1183            "AWS::ECS::Service" => Some(self.update_ecs_service(existing, new_def)?),
1184            "AWS::ECS::TaskDefinition" => Some(self.update_ecs_task_definition(existing, new_def)?),
1185            "AWS::ECS::CapacityProvider" => {
1186                Some(self.update_ecs_capacity_provider(existing, new_def)?)
1187            }
1188            "AWS::ECR::Repository" => Some(self.update_ecr_repository(existing, new_def)?),
1189            "AWS::ECR::RepositoryPolicy" => {
1190                Some(self.update_ecr_repository_policy(existing, new_def)?)
1191            }
1192            "AWS::ECR::LifecyclePolicy" => {
1193                Some(self.update_ecr_lifecycle_policy(existing, new_def)?)
1194            }
1195            "AWS::ECR::RegistryPolicy" => Some(self.update_ecr_registry_policy(existing, new_def)?),
1196            "AWS::ECR::ReplicationConfiguration" => {
1197                Some(self.update_ecr_replication_configuration(existing, new_def)?)
1198            }
1199            "AWS::ECR::RegistryScanningConfiguration" => {
1200                Some(self.update_ecr_registry_scanning_configuration(existing, new_def)?)
1201            }
1202            "AWS::ECR::PullThroughCacheRule" => {
1203                Some(self.update_ecr_pull_through_cache_rule(existing, new_def)?)
1204            }
1205            "AWS::KMS::Key" => Some(self.update_kms_key(existing, new_def)?),
1206            "AWS::KMS::ReplicaKey" => Some(self.update_kms_replica_key(existing, new_def)?),
1207            "AWS::KMS::Alias" => Some(self.update_kms_alias(existing, new_def)?),
1208            "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1209                Some(self.update_elbv2_load_balancer(existing, new_def)?)
1210            }
1211            "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1212                Some(self.update_elbv2_target_group(existing, new_def)?)
1213            }
1214            "AWS::ElasticLoadBalancingV2::Listener" => {
1215                Some(self.update_elbv2_listener(existing, new_def)?)
1216            }
1217            "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1218                Some(self.update_elbv2_listener_rule(existing, new_def)?)
1219            }
1220            "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
1221                Some(self.update_elbv2_listener_certificate(existing, new_def)?)
1222            }
1223            "AWS::ElasticLoadBalancingV2::TrustStore" => {
1224                Some(self.update_elbv2_trust_store(existing, new_def)?)
1225            }
1226            "AWS::CloudWatch::Alarm" => Some(self.update_cloudwatch_alarm(existing, new_def)?),
1227            "AWS::CloudWatch::Dashboard" => {
1228                Some(self.update_cloudwatch_dashboard(existing, new_def)?)
1229            }
1230            _ => None,
1231        };
1232
1233        Ok(result.map(|res| StackResource {
1234            logical_id: existing.logical_id.clone(),
1235            physical_id: res.physical_id,
1236            resource_type: existing.resource_type.clone(),
1237            status: "UPDATE_COMPLETE".to_string(),
1238            service_token: existing.service_token.clone(),
1239            attributes: res.attributes,
1240        }))
1241    }
1242
1243    /// Resolve a `Fn::GetAtt` against a previously provisioned resource.
1244    /// Returns the attribute value as a string, or `None` if the resource
1245    /// type doesn't expose that attribute (caller falls back to a placeholder
1246    /// so multi-pass provisioning can retry).
1247    ///
1248    /// The lookup first checks attributes captured at create time on the
1249    /// `StackResource`, then falls back to live service-state queries for
1250    /// the well-known attribute names of each resource type. This means
1251    /// attributes that change after creation (e.g. Lambda `FunctionUrl`)
1252    /// resolve correctly even when the URL was added in a separate pass.
1253    pub fn get_att(&self, resource: &StackResource, attribute: &str) -> Option<String> {
1254        // Captured attributes are the source of truth — they were computed
1255        // at create time and never go stale for the resources we ship today.
1256        if let Some(v) = resource.attributes.get(attribute) {
1257            return Some(v.clone());
1258        }
1259        // Live-state fallback for attributes that aren't pre-captured. This
1260        // is the extension point for future provisioners.
1261        match resource.resource_type.as_str() {
1262            "AWS::S3::Bucket" => self.get_att_s3_bucket(&resource.physical_id, attribute),
1263            "AWS::Lambda::Function" => {
1264                self.get_att_lambda_function(&resource.physical_id, attribute)
1265            }
1266            "AWS::IAM::Role" => self.get_att_iam_role(&resource.physical_id, attribute),
1267            "AWS::SQS::Queue" => self.get_att_sqs_queue(&resource.physical_id, attribute),
1268            "AWS::SNS::Topic" => self.get_att_sns_topic(&resource.physical_id, attribute),
1269            "AWS::DynamoDB::Table" => self.get_att_dynamodb_table(&resource.physical_id, attribute),
1270            "AWS::KMS::Key" => self.get_att_kms_key(&resource.physical_id, attribute),
1271            "AWS::SecretsManager::Secret" => {
1272                self.get_att_secrets_manager_secret(&resource.physical_id, attribute)
1273            }
1274            "AWS::CloudFront::Distribution" => {
1275                self.get_att_cf_distribution(&resource.physical_id, attribute)
1276            }
1277            "AWS::ECS::Cluster" => self.get_att_ecs_cluster(&resource.physical_id, attribute),
1278            "AWS::ECS::Service" => self.get_att_ecs_service(&resource.physical_id, attribute),
1279            "AWS::ECS::CapacityProvider" => {
1280                self.get_att_ecs_capacity_provider(&resource.physical_id, attribute)
1281            }
1282            "AWS::ECR::Repository" => self.get_att_ecr_repository(&resource.physical_id, attribute),
1283            "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1284                self.get_att_elbv2_load_balancer(&resource.physical_id, attribute)
1285            }
1286            "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1287                self.get_att_elbv2_target_group(&resource.physical_id, attribute)
1288            }
1289            "AWS::ElasticLoadBalancingV2::Listener" => {
1290                self.get_att_elbv2_listener(&resource.physical_id, attribute)
1291            }
1292            "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1293                self.get_att_elbv2_listener_rule(&resource.physical_id, attribute)
1294            }
1295            "AWS::ElasticLoadBalancingV2::TrustStore" => {
1296                self.get_att_elbv2_trust_store(&resource.physical_id, attribute)
1297            }
1298            "AWS::WAFv2::WebACL" => self.get_att_wafv2_web_acl(&resource.physical_id, attribute),
1299            "AWS::WAFv2::IPSet" => self.get_att_wafv2_ip_set(&resource.physical_id, attribute),
1300            "AWS::WAFv2::RegexPatternSet" => {
1301                self.get_att_wafv2_regex_pattern_set(&resource.physical_id, attribute)
1302            }
1303            "AWS::WAFv2::RuleGroup" => {
1304                self.get_att_wafv2_rule_group(&resource.physical_id, attribute)
1305            }
1306            "AWS::SES::ConfigurationSet" => {
1307                self.get_att_ses_configuration_set(&resource.physical_id, attribute)
1308            }
1309            "AWS::SES::EmailIdentity" => {
1310                self.get_att_ses_email_identity(&resource.physical_id, attribute)
1311            }
1312            "AWS::SES::Template" => self.get_att_ses_template(&resource.physical_id, attribute),
1313            "AWS::SES::ContactList" => {
1314                self.get_att_ses_contact_list(&resource.physical_id, attribute)
1315            }
1316            "AWS::SES::DedicatedIpPool" => {
1317                self.get_att_ses_dedicated_ip_pool(&resource.physical_id, attribute)
1318            }
1319            "AWS::SES::ReceiptRuleSet" => {
1320                self.get_att_ses_receipt_rule_set(&resource.physical_id, attribute)
1321            }
1322            "AWS::Athena::DataCatalog" => {
1323                self.get_att_athena_data_catalog(&resource.physical_id, attribute)
1324            }
1325            "AWS::Athena::NamedQuery" => {
1326                self.get_att_athena_named_query(&resource.physical_id, attribute)
1327            }
1328            "AWS::Athena::WorkGroup" => {
1329                self.get_att_athena_work_group(&resource.physical_id, attribute)
1330            }
1331            "AWS::Athena::PreparedStatement" => {
1332                self.get_att_athena_prepared_statement(&resource.physical_id, attribute)
1333            }
1334            "AWS::CloudFormation::Stack" => {
1335                self.get_att_cloudformation_stack(&resource.physical_id, attribute)
1336            }
1337            _ => None,
1338        }
1339    }
1340
1341    fn get_att_ses_configuration_set(&self, physical_id: &str, attribute: &str) -> Option<String> {
1342        let mut accounts = self.ses_state.write();
1343        let state = accounts.get_or_create(&self.account_id);
1344        let cs = state.configuration_sets.get(physical_id)?;
1345        match attribute {
1346            "Name" => Some(cs.name.clone()),
1347            _ => None,
1348        }
1349    }
1350
1351    fn get_att_ses_email_identity(&self, physical_id: &str, attribute: &str) -> Option<String> {
1352        let mut accounts = self.ses_state.write();
1353        let state = accounts.get_or_create(&self.account_id);
1354        let id = state.identities.get(physical_id)?;
1355        match attribute {
1356            "IdentityName" => Some(id.identity_name.clone()),
1357            _ => None,
1358        }
1359    }
1360
1361    fn get_att_ses_template(&self, physical_id: &str, attribute: &str) -> Option<String> {
1362        let mut accounts = self.ses_state.write();
1363        let state = accounts.get_or_create(&self.account_id);
1364        let tpl = state.templates.get(physical_id)?;
1365        match attribute {
1366            "TemplateName" => Some(tpl.template_name.clone()),
1367            _ => None,
1368        }
1369    }
1370
1371    fn get_att_ses_contact_list(&self, physical_id: &str, attribute: &str) -> Option<String> {
1372        let mut accounts = self.ses_state.write();
1373        let state = accounts.get_or_create(&self.account_id);
1374        let cl = state.contact_lists.get(physical_id)?;
1375        match attribute {
1376            "ContactListName" => Some(cl.contact_list_name.clone()),
1377            _ => None,
1378        }
1379    }
1380
1381    fn get_att_ses_dedicated_ip_pool(&self, physical_id: &str, attribute: &str) -> Option<String> {
1382        let mut accounts = self.ses_state.write();
1383        let state = accounts.get_or_create(&self.account_id);
1384        let pool = state.dedicated_ip_pools.get(physical_id)?;
1385        match attribute {
1386            "PoolName" => Some(pool.pool_name.clone()),
1387            _ => None,
1388        }
1389    }
1390
1391    fn get_att_ses_receipt_rule_set(&self, physical_id: &str, attribute: &str) -> Option<String> {
1392        let mut accounts = self.ses_state.write();
1393        let state = accounts.get_or_create(&self.account_id);
1394        let rs = state.receipt_rule_sets.get(physical_id)?;
1395        match attribute {
1396            "RuleSetName" => Some(rs.name.clone()),
1397            _ => None,
1398        }
1399    }
1400
1401    fn get_att_wafv2_web_acl(&self, physical_id: &str, attribute: &str) -> Option<String> {
1402        let mut accounts = self.wafv2_state.write();
1403        let state = accounts
1404            .accounts
1405            .entry(self.account_id.clone())
1406            .or_default();
1407        let acl = state.web_acls.values().find(|a| a.arn == physical_id)?;
1408        match attribute {
1409            "Arn" => Some(acl.arn.clone()),
1410            "Id" => Some(acl.id.clone()),
1411            "Name" => Some(acl.name.clone()),
1412            "LabelNamespace" => Some(acl.label_namespace.clone()),
1413            "Capacity" => Some(acl.capacity.to_string()),
1414            _ => None,
1415        }
1416    }
1417
1418    fn get_att_wafv2_ip_set(&self, physical_id: &str, attribute: &str) -> Option<String> {
1419        let mut accounts = self.wafv2_state.write();
1420        let state = accounts
1421            .accounts
1422            .entry(self.account_id.clone())
1423            .or_default();
1424        let ip_set = state.ip_sets.values().find(|i| i.arn == physical_id)?;
1425        match attribute {
1426            "Arn" => Some(ip_set.arn.clone()),
1427            "Id" => Some(ip_set.id.clone()),
1428            "Name" => Some(ip_set.name.clone()),
1429            _ => None,
1430        }
1431    }
1432
1433    fn get_att_wafv2_regex_pattern_set(
1434        &self,
1435        physical_id: &str,
1436        attribute: &str,
1437    ) -> Option<String> {
1438        let mut accounts = self.wafv2_state.write();
1439        let state = accounts
1440            .accounts
1441            .entry(self.account_id.clone())
1442            .or_default();
1443        let set = state
1444            .regex_pattern_sets
1445            .values()
1446            .find(|r| r.arn == physical_id)?;
1447        match attribute {
1448            "Arn" => Some(set.arn.clone()),
1449            "Id" => Some(set.id.clone()),
1450            "Name" => Some(set.name.clone()),
1451            _ => None,
1452        }
1453    }
1454
1455    fn get_att_wafv2_rule_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
1456        let mut accounts = self.wafv2_state.write();
1457        let state = accounts
1458            .accounts
1459            .entry(self.account_id.clone())
1460            .or_default();
1461        let rg = state.rule_groups.values().find(|r| r.arn == physical_id)?;
1462        match attribute {
1463            "Arn" => Some(rg.arn.clone()),
1464            "Id" => Some(rg.id.clone()),
1465            "Name" => Some(rg.name.clone()),
1466            _ => None,
1467        }
1468    }
1469
1470    fn get_att_s3_bucket(&self, physical_id: &str, attribute: &str) -> Option<String> {
1471        let mut accounts = self.s3_state.write();
1472        let state = accounts.get_or_create(&self.account_id);
1473        let bucket = state.buckets.get(physical_id)?;
1474        match attribute {
1475            "Arn" => Some(format!("arn:aws:s3:::{}", bucket.name)),
1476            "DomainName" => Some(format!("{}.s3.amazonaws.com", bucket.name)),
1477            "RegionalDomainName" => {
1478                Some(format!("{}.s3.{}.amazonaws.com", bucket.name, self.region))
1479            }
1480            "DualStackDomainName" => Some(format!(
1481                "{}.s3.dualstack.{}.amazonaws.com",
1482                bucket.name, self.region
1483            )),
1484            "WebsiteURL" => Some(format!(
1485                "http://{}.s3-website-{}.amazonaws.com",
1486                bucket.name, self.region
1487            )),
1488            _ => None,
1489        }
1490    }
1491
1492    fn get_att_lambda_function(&self, physical_id: &str, attribute: &str) -> Option<String> {
1493        let function_name = parse_lambda_function_name(physical_id);
1494        let mut accounts = self.lambda_state.write();
1495        let state = accounts.get_or_create(&self.account_id);
1496        match attribute {
1497            "Arn" => state
1498                .functions
1499                .get(&function_name)
1500                .map(|f| f.function_arn.clone()),
1501            "FunctionUrl" => state
1502                .function_url_configs
1503                .get(&function_name)
1504                .map(|u| u.function_url.clone()),
1505            "Version" => state
1506                .functions
1507                .get(&function_name)
1508                .map(|f| f.version.clone()),
1509            _ => None,
1510        }
1511    }
1512
1513    fn get_att_iam_role(&self, physical_id: &str, attribute: &str) -> Option<String> {
1514        let mut accounts = self.iam_state.write();
1515        let state = accounts.get_or_create(&self.account_id);
1516        // physical_id may be the role ARN or the role name depending on how
1517        // provisioning resolved Ref. Try both shapes.
1518        let role = state
1519            .roles
1520            .values()
1521            .find(|r| r.arn == physical_id || r.role_name == physical_id)?;
1522        match attribute {
1523            "Arn" => Some(role.arn.clone()),
1524            "RoleId" => Some(role.role_id.clone()),
1525            _ => None,
1526        }
1527    }
1528
1529    fn get_att_sqs_queue(&self, physical_id: &str, attribute: &str) -> Option<String> {
1530        let mut accounts = self.sqs_state.write();
1531        let state = accounts.get_or_create(&self.account_id);
1532        let queue = state.queues.get(physical_id)?;
1533        match attribute {
1534            "Arn" => Some(queue.arn.clone()),
1535            "QueueName" => Some(queue.queue_name.clone()),
1536            "QueueUrl" => Some(queue.queue_url.clone()),
1537            _ => None,
1538        }
1539    }
1540
1541    fn get_att_sns_topic(&self, physical_id: &str, attribute: &str) -> Option<String> {
1542        let mut accounts = self.sns_state.write();
1543        let state = accounts.get_or_create(&self.account_id);
1544        let topic = state.topics.get(physical_id)?;
1545        match attribute {
1546            "TopicArn" => Some(topic.topic_arn.clone()),
1547            "TopicName" => Some(topic.name.clone()),
1548            _ => None,
1549        }
1550    }
1551
1552    fn get_att_dynamodb_table(&self, physical_id: &str, attribute: &str) -> Option<String> {
1553        let mut accounts = self.dynamodb_state.write();
1554        let state = accounts.get_or_create(&self.account_id);
1555        let table = state.tables.get(physical_id)?;
1556        match attribute {
1557            "Arn" => Some(table.arn.clone()),
1558            "StreamArn" => table.stream_arn.clone(),
1559            _ => None,
1560        }
1561    }
1562
1563    fn get_att_kms_key(&self, physical_id: &str, attribute: &str) -> Option<String> {
1564        let mut accounts = self.kms_state.write();
1565        let state = accounts.get_or_create(&self.account_id);
1566        let key = state.keys.get(physical_id)?;
1567        match attribute {
1568            "Arn" => Some(key.arn.clone()),
1569            "KeyId" => Some(key.key_id.clone()),
1570            _ => None,
1571        }
1572    }
1573
1574    fn get_att_secrets_manager_secret(&self, physical_id: &str, attribute: &str) -> Option<String> {
1575        let mut accounts = self.secretsmanager_state.write();
1576        let state = accounts.get_or_create(&self.account_id);
1577        let secret = state.secrets.get(physical_id)?;
1578        match attribute {
1579            // Secrets Manager's CFN doc treats Id and Arn interchangeably —
1580            // both resolve to the secret ARN.
1581            "Arn" | "Id" => Some(secret.arn.clone()),
1582            _ => None,
1583        }
1584    }
1585
1586    fn get_att_cf_distribution(&self, physical_id: &str, attribute: &str) -> Option<String> {
1587        // CloudFront state is keyed under a fixed "000000000000" account in the
1588        // fakecloud_cloudfront crate; matching create_cf_distribution above.
1589        let accounts = self.cloudfront_state.read();
1590        let state = accounts.get("000000000000")?;
1591        let dist = state.distributions.get(physical_id)?;
1592        match attribute {
1593            "DomainName" => Some(dist.domain_name.clone()),
1594            "Id" => Some(dist.id.clone()),
1595            _ => None,
1596        }
1597    }
1598
1599    /// Delete a previously created resource.
1600    pub fn delete_resource(&self, resource: &StackResource) -> Result<(), String> {
1601        match resource.resource_type.as_str() {
1602            "AWS::SQS::Queue" => self.delete_sqs_queue(&resource.physical_id),
1603            "AWS::SNS::Topic" => self.delete_sns_topic(&resource.physical_id),
1604            "AWS::SNS::Subscription" => self.delete_sns_subscription(&resource.physical_id),
1605            "AWS::SSM::Parameter" => self.delete_ssm_parameter(&resource.physical_id),
1606            "AWS::IAM::Role" => self.delete_iam_role(&resource.physical_id),
1607            "AWS::IAM::Policy" => self.delete_iam_policy(&resource.physical_id),
1608            "AWS::IAM::User" => self.delete_iam_user(&resource.physical_id),
1609            "AWS::IAM::Group" => self.delete_iam_group(&resource.physical_id),
1610            "AWS::IAM::ManagedPolicy" => self.delete_iam_managed_policy(&resource.physical_id),
1611            "AWS::IAM::UserToGroupAddition" => {
1612                self.delete_iam_user_to_group_addition(&resource.physical_id)
1613            }
1614            "AWS::IAM::AccessKey" => self.delete_iam_access_key(&resource.physical_id),
1615            "AWS::IAM::InstanceProfile" => self.delete_iam_instance_profile(&resource.physical_id),
1616            "AWS::IAM::OIDCProvider" => self.delete_iam_oidc_provider(&resource.physical_id),
1617            "AWS::IAM::SAMLProvider" => self.delete_iam_saml_provider(&resource.physical_id),
1618            "AWS::IAM::ServiceLinkedRole" => {
1619                self.delete_iam_service_linked_role(&resource.physical_id)
1620            }
1621            "AWS::IAM::VirtualMFADevice" => {
1622                self.delete_iam_virtual_mfa_device(&resource.physical_id)
1623            }
1624            "AWS::S3::Bucket" => self.delete_s3_bucket(&resource.physical_id),
1625            "AWS::Events::Rule" => self.delete_eventbridge_rule(&resource.physical_id),
1626            "AWS::Events::Connection" => self.delete_eventbridge_connection(&resource.physical_id),
1627            "AWS::Events::EventBus" => self.delete_eventbridge_event_bus(&resource.physical_id),
1628            "AWS::Events::EventBusPolicy" => {
1629                self.delete_eventbridge_event_bus_policy(&resource.physical_id)
1630            }
1631            "AWS::Events::Endpoint" => self.delete_eventbridge_endpoint(&resource.physical_id),
1632            "AWS::Events::ApiDestination" => {
1633                self.delete_eventbridge_api_destination(&resource.physical_id)
1634            }
1635            "AWS::Events::Archive" => self.delete_eventbridge_archive(&resource.physical_id),
1636            "AWS::DynamoDB::Table" => self.delete_dynamodb_table(&resource.physical_id),
1637            "AWS::Logs::LogGroup" => self.delete_log_group(&resource.physical_id),
1638            "AWS::Logs::LogStream" => self.delete_log_stream(&resource.physical_id),
1639            "AWS::Logs::MetricFilter" => self.delete_metric_filter(&resource.physical_id),
1640            "AWS::Logs::SubscriptionFilter" => {
1641                self.delete_subscription_filter(&resource.physical_id)
1642            }
1643            "AWS::Logs::Destination" => self.delete_logs_destination(&resource.physical_id),
1644            "AWS::Logs::ResourcePolicy" => self.delete_logs_resource_policy(&resource.physical_id),
1645            "AWS::Logs::QueryDefinition" => {
1646                self.delete_logs_query_definition(&resource.physical_id)
1647            }
1648            "AWS::Logs::Delivery" => self.delete_logs_delivery(&resource.physical_id),
1649            "AWS::Logs::DeliveryDestination" => {
1650                self.delete_logs_delivery_destination(&resource.physical_id)
1651            }
1652            "AWS::Logs::DeliverySource" => self.delete_logs_delivery_source(&resource.physical_id),
1653            "AWS::Lambda::Function" => self.delete_lambda_function(&resource.physical_id),
1654            "AWS::Lambda::Permission" => self.delete_lambda_permission(&resource.physical_id),
1655            "AWS::Lambda::EventSourceMapping" => {
1656                self.delete_lambda_event_source_mapping(&resource.physical_id)
1657            }
1658            "AWS::Lambda::LayerVersion" => self.delete_lambda_layer_version(&resource.physical_id),
1659            "AWS::Lambda::Url" => self.delete_lambda_url(&resource.physical_id),
1660            "AWS::Lambda::Alias" => self.delete_lambda_alias(&resource.physical_id),
1661            "AWS::Lambda::Version" => self.delete_lambda_version(&resource.physical_id),
1662            "AWS::SecretsManager::Secret" => {
1663                self.delete_secrets_manager_secret(&resource.physical_id)
1664            }
1665            "AWS::Kinesis::Stream" => self.delete_kinesis_stream(&resource.physical_id),
1666            "AWS::Kinesis::StreamConsumer" => {
1667                self.delete_kinesis_stream_consumer(&resource.physical_id)
1668            }
1669            "AWS::KMS::Key" => self.delete_kms_key(&resource.physical_id),
1670            "AWS::KMS::ReplicaKey" => self.delete_kms_replica_key(&resource.physical_id),
1671            "AWS::KMS::Alias" => self.delete_kms_alias(&resource.physical_id),
1672            "AWS::ECR::Repository" => self.delete_ecr_repository(&resource.physical_id),
1673            "AWS::ECR::RepositoryPolicy" => {
1674                self.delete_ecr_repository_policy(&resource.physical_id)
1675            }
1676            "AWS::ECR::LifecyclePolicy" => self.delete_ecr_lifecycle_policy(&resource.physical_id),
1677            "AWS::ECR::RegistryPolicy" => self.delete_ecr_registry_policy(),
1678            "AWS::ECR::ReplicationConfiguration" => self.delete_ecr_replication_configuration(),
1679            "AWS::ECR::RegistryScanningConfiguration" => {
1680                self.delete_ecr_registry_scanning_configuration()
1681            }
1682            "AWS::ECR::PullThroughCacheRule" => {
1683                self.delete_ecr_pull_through_cache_rule(&resource.physical_id)
1684            }
1685            "AWS::CloudWatch::Alarm" => self.delete_cloudwatch_alarm(&resource.physical_id),
1686            "AWS::CloudWatch::Dashboard" => self.delete_cloudwatch_dashboard(&resource.physical_id),
1687            "AWS::ElasticLoadBalancingV2::LoadBalancer" => {
1688                self.delete_elbv2_load_balancer(&resource.physical_id)
1689            }
1690            "AWS::ElasticLoadBalancingV2::TargetGroup" => {
1691                self.delete_elbv2_target_group(&resource.physical_id)
1692            }
1693            "AWS::ElasticLoadBalancingV2::Listener" => {
1694                self.delete_elbv2_listener(&resource.physical_id)
1695            }
1696            "AWS::ElasticLoadBalancingV2::ListenerRule" => {
1697                self.delete_elbv2_listener_rule(&resource.physical_id)
1698            }
1699            "AWS::ElasticLoadBalancingV2::ListenerCertificate" => {
1700                self.delete_elbv2_listener_certificate(&resource.physical_id)
1701            }
1702            "AWS::ElasticLoadBalancingV2::TrustStore" => {
1703                self.delete_elbv2_trust_store(&resource.physical_id)
1704            }
1705            "AWS::Organizations::Organization" => self.delete_organization(&resource.physical_id),
1706            "AWS::Organizations::OrganizationalUnit" => {
1707                self.delete_organization_unit(&resource.physical_id)
1708            }
1709            "AWS::Organizations::Account" => {
1710                self.delete_organization_account(&resource.physical_id)
1711            }
1712            "AWS::Organizations::Policy" => self.delete_organization_policy(&resource.physical_id),
1713            "AWS::Organizations::ResourcePolicy" => {
1714                self.delete_organization_resource_policy(&resource.physical_id)
1715            }
1716            "AWS::Cognito::UserPool" => self.delete_cognito_user_pool(&resource.physical_id),
1717            "AWS::Cognito::UserPoolClient" => {
1718                self.delete_cognito_user_pool_client(&resource.physical_id)
1719            }
1720            "AWS::Cognito::UserPoolDomain" => {
1721                self.delete_cognito_user_pool_domain(&resource.physical_id)
1722            }
1723            "AWS::Cognito::IdentityPool" => {
1724                self.delete_cognito_identity_pool(&resource.physical_id)
1725            }
1726            "AWS::Cognito::IdentityPoolRoleAttachment" => {
1727                self.delete_cognito_identity_pool_role_attachment(&resource.physical_id)
1728            }
1729            "AWS::RDS::DBSubnetGroup" => self.delete_rds_subnet_group(&resource.physical_id),
1730            "AWS::RDS::DBParameterGroup" => self.delete_rds_parameter_group(&resource.physical_id),
1731            "AWS::RDS::DBClusterParameterGroup" => {
1732                self.delete_rds_cluster_parameter_group(&resource.physical_id)
1733            }
1734            "AWS::RDS::OptionGroup" => self.delete_rds_option_group(&resource.physical_id),
1735            "AWS::RDS::EventSubscription" => {
1736                self.delete_rds_event_subscription(&resource.physical_id)
1737            }
1738            "AWS::RDS::DBSecurityGroup" => self.delete_rds_security_group(&resource.physical_id),
1739            "AWS::RDS::DBProxy" => self.delete_rds_db_proxy(&resource.physical_id),
1740            "AWS::RDS::DBInstance" => self.delete_rds_db_instance(&resource.physical_id),
1741            "AWS::RDS::DBCluster" => self.delete_rds_db_cluster(&resource.physical_id),
1742            "AWS::ECS::Cluster" => self.delete_ecs_cluster(&resource.physical_id),
1743            "AWS::ECS::TaskDefinition" => self.delete_ecs_task_definition(&resource.physical_id),
1744            "AWS::ECS::Service" => self.delete_ecs_service(&resource.physical_id),
1745            "AWS::ECS::CapacityProvider" => {
1746                self.delete_ecs_capacity_provider(&resource.physical_id)
1747            }
1748            "AWS::CertificateManager::Certificate" => {
1749                self.delete_acm_certificate(&resource.physical_id)
1750            }
1751            "AWS::CertificateManager::Account" => self.delete_acm_account(),
1752            "AWS::ElastiCache::ParameterGroup" => {
1753                self.delete_ec_parameter_group(&resource.physical_id)
1754            }
1755            "AWS::ElastiCache::SubnetGroup" => self.delete_ec_subnet_group(&resource.physical_id),
1756            "AWS::ElastiCache::SecurityGroup" => {
1757                self.delete_ec_security_group(&resource.physical_id)
1758            }
1759            "AWS::ElastiCache::User" => self.delete_ec_user(&resource.physical_id),
1760            "AWS::ElastiCache::UserGroup" => self.delete_ec_user_group(&resource.physical_id),
1761            "AWS::ElastiCache::CacheCluster" => self.delete_ec_cache_cluster(&resource.physical_id),
1762            "AWS::ElastiCache::ReplicationGroup" => {
1763                self.delete_ec_replication_group(&resource.physical_id)
1764            }
1765            "AWS::Route53::HostedZone" => self.delete_route53_hosted_zone(&resource.physical_id),
1766            "AWS::Route53::RecordSet" => {
1767                self.delete_route53_record_set(&resource.physical_id, &resource.attributes)
1768            }
1769            "AWS::Route53::HealthCheck" => self.delete_route53_health_check(&resource.physical_id),
1770            "AWS::Route53::DNSSEC" => self.delete_route53_dnssec(&resource.physical_id),
1771            "AWS::Route53::KeySigningKey" => {
1772                self.delete_route53_key_signing_key(&resource.physical_id)
1773            }
1774            "AWS::CloudFront::CloudFrontOriginAccessIdentity" => {
1775                self.delete_cf_origin_access_identity(&resource.physical_id)
1776            }
1777            "AWS::CloudFront::Distribution" => self.delete_cf_distribution(&resource.physical_id),
1778            "AWS::CloudFront::OriginAccessControl" => {
1779                self.delete_cf_origin_access_control(&resource.physical_id)
1780            }
1781            "AWS::CloudFront::PublicKey" => self.delete_cf_public_key(&resource.physical_id),
1782            "AWS::CloudFront::KeyGroup" => self.delete_cf_key_group(&resource.physical_id),
1783            "AWS::CloudFront::Function" => self.delete_cf_function(&resource.physical_id),
1784            "AWS::CloudFront::CachePolicy" => self.delete_cf_cache_policy(&resource.physical_id),
1785            "AWS::CloudFront::OriginRequestPolicy" => {
1786                self.delete_cf_origin_request_policy(&resource.physical_id)
1787            }
1788            "AWS::CloudFront::ResponseHeadersPolicy" => {
1789                self.delete_cf_response_headers_policy(&resource.physical_id)
1790            }
1791            "AWS::StepFunctions::StateMachine" => {
1792                self.delete_sfn_state_machine(&resource.physical_id)
1793            }
1794            "AWS::StepFunctions::Activity" => self.delete_sfn_activity(&resource.physical_id),
1795            "AWS::StepFunctions::StateMachineVersion" => {
1796                self.delete_sfn_version(&resource.physical_id)
1797            }
1798            "AWS::StepFunctions::StateMachineAlias" => self.delete_sfn_alias(&resource.physical_id),
1799            "AWS::WAFv2::WebACL" => self.delete_wafv2_web_acl(&resource.physical_id),
1800            "AWS::WAFv2::IPSet" => self.delete_wafv2_ip_set(&resource.physical_id),
1801            "AWS::WAFv2::RegexPatternSet" => {
1802                self.delete_wafv2_regex_pattern_set(&resource.physical_id)
1803            }
1804            "AWS::WAFv2::RuleGroup" => self.delete_wafv2_rule_group(&resource.physical_id),
1805            "AWS::WAFv2::LoggingConfiguration" => {
1806                self.delete_wafv2_logging_configuration(&resource.physical_id)
1807            }
1808            "AWS::WAFv2::WebACLAssociation" => {
1809                self.delete_wafv2_web_acl_association(&resource.physical_id)
1810            }
1811            "AWS::ApiGateway::RestApi" => self.delete_apigw_rest_api(&resource.physical_id),
1812            "AWS::ApiGateway::Resource" => {
1813                self.delete_apigw_resource(&resource.physical_id, &resource.attributes)
1814            }
1815            "AWS::ApiGateway::Method" => self.delete_apigw_method(&resource.physical_id),
1816            "AWS::ApiGateway::Deployment" => {
1817                self.delete_apigw_deployment(&resource.physical_id, &resource.attributes)
1818            }
1819            "AWS::ApiGateway::Stage" => {
1820                self.delete_apigw_stage(&resource.physical_id, &resource.attributes)
1821            }
1822            "AWS::ApiGateway::Authorizer" => {
1823                self.delete_apigw_authorizer(&resource.physical_id, &resource.attributes)
1824            }
1825            "AWS::ApiGateway::RequestValidator" => {
1826                self.delete_apigw_request_validator(&resource.physical_id, &resource.attributes)
1827            }
1828            "AWS::ApiGateway::Model" => {
1829                self.delete_apigw_model(&resource.physical_id, &resource.attributes)
1830            }
1831            "AWS::ApiGateway::GatewayResponse" => {
1832                self.delete_apigw_gateway_response(&resource.physical_id, &resource.attributes)
1833            }
1834            "AWS::ApiGateway::UsagePlan" => self.delete_apigw_usage_plan(&resource.physical_id),
1835            "AWS::ApiGateway::ApiKey" => self.delete_apigw_api_key(&resource.physical_id),
1836            "AWS::ApiGateway::UsagePlanKey" => {
1837                self.delete_apigw_usage_plan_key(&resource.physical_id, &resource.attributes)
1838            }
1839            "AWS::ApiGateway::DomainName" => self.delete_apigw_domain_name(&resource.physical_id),
1840            "AWS::ApiGateway::BasePathMapping" => {
1841                self.delete_apigw_base_path_mapping(&resource.physical_id, &resource.attributes)
1842            }
1843            "AWS::ApiGatewayV2::Api" => self.delete_apigwv2_api(&resource.physical_id),
1844            "AWS::ApiGatewayV2::Route" => {
1845                self.delete_apigwv2_route(&resource.physical_id, &resource.attributes)
1846            }
1847            "AWS::ApiGatewayV2::Integration" => {
1848                self.delete_apigwv2_integration(&resource.physical_id, &resource.attributes)
1849            }
1850            "AWS::ApiGatewayV2::IntegrationResponse" => self
1851                .delete_apigwv2_integration_response(&resource.physical_id, &resource.attributes),
1852            "AWS::ApiGatewayV2::RouteResponse" => {
1853                self.delete_apigwv2_route_response(&resource.physical_id, &resource.attributes)
1854            }
1855            "AWS::ApiGatewayV2::Stage" => {
1856                self.delete_apigwv2_stage(&resource.physical_id, &resource.attributes)
1857            }
1858            "AWS::ApiGatewayV2::Deployment" => {
1859                self.delete_apigwv2_deployment(&resource.physical_id, &resource.attributes)
1860            }
1861            "AWS::ApiGatewayV2::Authorizer" => {
1862                self.delete_apigwv2_authorizer(&resource.physical_id, &resource.attributes)
1863            }
1864            "AWS::ApiGatewayV2::DomainName" => {
1865                self.delete_apigwv2_domain_name(&resource.physical_id)
1866            }
1867            "AWS::ApiGatewayV2::ApiMapping" => {
1868                self.delete_apigwv2_api_mapping(&resource.physical_id, &resource.attributes)
1869            }
1870            "AWS::ApiGatewayV2::VpcLink" => self.delete_apigwv2_vpc_link(&resource.physical_id),
1871            "AWS::ApiGatewayV2::Model" => {
1872                self.delete_apigwv2_model(&resource.physical_id, &resource.attributes)
1873            }
1874            "AWS::SES::ConfigurationSet" => {
1875                self.delete_ses_configuration_set(&resource.physical_id)
1876            }
1877            "AWS::SES::ConfigurationSetEventDestination" => {
1878                self.delete_ses_event_destination(&resource.physical_id, &resource.attributes)
1879            }
1880            "AWS::SES::EmailIdentity" => self.delete_ses_email_identity(&resource.physical_id),
1881            "AWS::SES::Template" => self.delete_ses_template(&resource.physical_id),
1882            "AWS::SES::ContactList" => self.delete_ses_contact_list(&resource.physical_id),
1883            "AWS::SES::DedicatedIpPool" => self.delete_ses_dedicated_ip_pool(&resource.physical_id),
1884            "AWS::SES::ReceiptRule" => {
1885                self.delete_ses_receipt_rule(&resource.physical_id, &resource.attributes)
1886            }
1887            "AWS::SES::ReceiptRuleSet" => self.delete_ses_receipt_rule_set(&resource.physical_id),
1888            "AWS::SES::ReceiptFilter" => self.delete_ses_receipt_filter(&resource.physical_id),
1889            "AWS::SES::VdmAttributes" => Ok(()),
1890            "AWS::SecretsManager::RotationSchedule" => {
1891                self.delete_secrets_manager_rotation_schedule(&resource.physical_id)
1892            }
1893            "AWS::SecretsManager::ResourcePolicy" => {
1894                self.delete_secrets_manager_resource_policy(&resource.physical_id)
1895            }
1896            "AWS::SecretsManager::SecretTargetAttachment" => Ok(()),
1897            "AWS::ApplicationAutoScaling::ScalableTarget" => self
1898                .delete_application_autoscaling_scalable_target(
1899                    &resource.physical_id,
1900                    &resource.attributes,
1901                ),
1902            "AWS::ApplicationAutoScaling::ScalingPolicy" => self
1903                .delete_application_autoscaling_scaling_policy(
1904                    &resource.physical_id,
1905                    &resource.attributes,
1906                ),
1907            "AWS::Athena::DataCatalog" => self.delete_athena_data_catalog(&resource.physical_id),
1908            "AWS::Athena::NamedQuery" => self.delete_athena_named_query(&resource.physical_id),
1909            "AWS::Athena::WorkGroup" => self.delete_athena_work_group(&resource.physical_id),
1910            "AWS::Athena::PreparedStatement" => {
1911                self.delete_athena_prepared_statement(&resource.physical_id, &resource.attributes)
1912            }
1913            "AWS::KinesisFirehose::DeliveryStream" => {
1914                self.delete_firehose_delivery_stream(&resource.physical_id)
1915            }
1916            "AWS::Glue::Database" => self.delete_glue_database(&resource.physical_id),
1917            "AWS::CloudFormation::Stack" => self.delete_cloudformation_stack(&resource.physical_id),
1918            "AWS::Glue::Table" => self.delete_glue_table(&resource.physical_id),
1919            "AWS::Glue::Partition" => {
1920                self.delete_glue_partition(&resource.physical_id, &resource.attributes)
1921            }
1922            t if t.starts_with("Custom::") || t == "AWS::CloudFormation::CustomResource" => {
1923                self.delete_custom_resource(resource)
1924            }
1925            other => Err(format!("Unsupported resource type: {other}")),
1926        }
1927    }
1928
1929    // --- SQS ---
1930
1931    fn create_sqs_queue(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
1932        let props = &resource.properties;
1933        let queue_name = props
1934            .get("QueueName")
1935            .and_then(|v| v.as_str())
1936            .unwrap_or(&resource.logical_id);
1937
1938        let mut __sqs_mas = self.sqs_state.write();
1939        let state = __sqs_mas.get_or_create(&self.account_id);
1940        let queue_url = format!("{}/{}/{}", state.endpoint, state.account_id, queue_name);
1941        let arn = format!(
1942            "arn:aws:sqs:{}:{}:{}",
1943            state.region, state.account_id, queue_name
1944        );
1945
1946        let is_fifo = queue_name.ends_with(".fifo");
1947        let mut attributes = std::collections::BTreeMap::new();
1948        if let Some(obj) = props.as_object() {
1949            for (k, v) in obj {
1950                if k != "QueueName" {
1951                    if let Some(s) = v.as_str() {
1952                        attributes.insert(k.clone(), s.to_string());
1953                    } else if let Some(n) = v.as_i64() {
1954                        attributes.insert(k.clone(), n.to_string());
1955                    }
1956                }
1957            }
1958        }
1959
1960        let queue = SqsQueue {
1961            queue_name: queue_name.to_string(),
1962            queue_url: queue_url.clone(),
1963            arn: arn.clone(),
1964            created_at: Utc::now(),
1965            messages: std::collections::VecDeque::new(),
1966            inflight: Vec::new(),
1967            attributes,
1968            is_fifo,
1969            dedup_cache: std::collections::BTreeMap::new(),
1970            redrive_policy: None,
1971            tags: std::collections::BTreeMap::new(),
1972            next_sequence_number: 0,
1973            permission_labels: Vec::new(),
1974            receipt_handle_map: std::collections::BTreeMap::new(),
1975        };
1976
1977        state
1978            .name_to_url
1979            .insert(queue_name.to_string(), queue_url.clone());
1980        state.queues.insert(queue_url.clone(), queue);
1981
1982        Ok(ProvisionResult::new(queue_url.clone())
1983            .with("Arn", arn)
1984            .with("QueueName", queue_name)
1985            .with("QueueUrl", queue_url))
1986    }
1987
1988    fn delete_sqs_queue(&self, physical_id: &str) -> Result<(), String> {
1989        let mut __sqs_mas = self.sqs_state.write();
1990        let state = __sqs_mas.get_or_create(&self.account_id);
1991        if let Some(queue) = state.queues.remove(physical_id) {
1992            state.name_to_url.remove(&queue.queue_name);
1993        }
1994        Ok(())
1995    }
1996
1997    // --- SNS ---
1998
1999    fn create_sns_topic(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
2000        let props = &resource.properties;
2001        let topic_name = props
2002            .get("TopicName")
2003            .and_then(|v| v.as_str())
2004            .unwrap_or(&resource.logical_id);
2005
2006        let mut __sns_mas = self.sns_state.write();
2007        let state = __sns_mas.get_or_create(&self.account_id);
2008        let topic_arn = format!(
2009            "arn:aws:sns:{}:{}:{}",
2010            state.region, state.account_id, topic_name
2011        );
2012
2013        let topic = SnsTopic {
2014            topic_arn: topic_arn.clone(),
2015            name: topic_name.to_string(),
2016            attributes: BTreeMap::new(),
2017            tags: Vec::new(),
2018            is_fifo: topic_name.ends_with(".fifo"),
2019            created_at: Utc::now(),
2020            subscriptions_deleted: 0,
2021        };
2022
2023        state.topics.insert(topic_arn.clone(), topic);
2024        Ok(ProvisionResult::new(topic_arn.clone())
2025            .with("TopicArn", topic_arn)
2026            .with("TopicName", topic_name))
2027    }
2028
2029    fn delete_sns_topic(&self, physical_id: &str) -> Result<(), String> {
2030        let mut __sns_mas = self.sns_state.write();
2031        let state = __sns_mas.get_or_create(&self.account_id);
2032        state.topics.remove(physical_id);
2033        // Also remove subscriptions for this topic
2034        state
2035            .subscriptions
2036            .retain(|_, sub| sub.topic_arn != physical_id);
2037        Ok(())
2038    }
2039
2040    // --- SNS Subscription ---
2041
2042    fn create_sns_subscription(
2043        &self,
2044        resource: &ResourceDefinition,
2045    ) -> Result<ProvisionResult, String> {
2046        let props = &resource.properties;
2047        let topic_arn = props
2048            .get("TopicArn")
2049            .and_then(|v| v.as_str())
2050            .ok_or("SNS Subscription requires TopicArn")?;
2051        let protocol = props
2052            .get("Protocol")
2053            .and_then(|v| v.as_str())
2054            .ok_or("SNS Subscription requires Protocol")?;
2055        let endpoint = props
2056            .get("Endpoint")
2057            .and_then(|v| v.as_str())
2058            .ok_or("SNS Subscription requires Endpoint")?;
2059
2060        let mut __sns_mas = self.sns_state.write();
2061        let state = __sns_mas.get_or_create(&self.account_id);
2062
2063        // Validate that the topic exists
2064        if !state.topics.contains_key(topic_arn) {
2065            return Err(format!("Topic ARN does not exist: {topic_arn}"));
2066        }
2067
2068        let sub_arn = format!("{}:{}", topic_arn, Uuid::new_v4());
2069
2070        let subscription = SnsSubscription {
2071            subscription_arn: sub_arn.clone(),
2072            topic_arn: topic_arn.to_string(),
2073            protocol: protocol.to_string(),
2074            endpoint: endpoint.to_string(),
2075            owner: state.account_id.clone(),
2076            attributes: BTreeMap::new(),
2077            confirmed: true,
2078            confirmation_token: None,
2079        };
2080
2081        state.subscriptions.insert(sub_arn.clone(), subscription);
2082        Ok(ProvisionResult::new(sub_arn.clone()).with("Arn", sub_arn))
2083    }
2084
2085    fn delete_sns_subscription(&self, physical_id: &str) -> Result<(), String> {
2086        let mut __sns_mas = self.sns_state.write();
2087        let state = __sns_mas.get_or_create(&self.account_id);
2088        state.subscriptions.remove(physical_id);
2089        Ok(())
2090    }
2091
2092    // --- SSM ---
2093
2094    fn create_ssm_parameter(
2095        &self,
2096        resource: &ResourceDefinition,
2097    ) -> Result<ProvisionResult, String> {
2098        let props = &resource.properties;
2099        let name = props
2100            .get("Name")
2101            .and_then(|v| v.as_str())
2102            .ok_or("SSM Parameter requires Name")?;
2103        let value = props
2104            .get("Value")
2105            .and_then(|v| v.as_str())
2106            .ok_or("SSM Parameter requires Value")?;
2107        let param_type = props
2108            .get("Type")
2109            .and_then(|v| v.as_str())
2110            .unwrap_or("String");
2111
2112        let mut accounts = self.ssm_state.write();
2113        let state = accounts.get_or_create(&self.account_id);
2114        let arn = format!(
2115            "arn:aws:ssm:{}:{}:parameter{}",
2116            self.region,
2117            self.account_id,
2118            if name.starts_with('/') {
2119                name.to_string()
2120            } else {
2121                format!("/{name}")
2122            }
2123        );
2124
2125        let parameter = SsmParameter {
2126            name: name.to_string(),
2127            value: value.to_string(),
2128            param_type: param_type.to_string(),
2129            version: 1,
2130            arn: arn.clone(),
2131            last_modified: Utc::now(),
2132            history: Vec::new(),
2133            tags: BTreeMap::new(),
2134            labels: BTreeMap::new(),
2135            description: props
2136                .get("Description")
2137                .and_then(|v| v.as_str())
2138                .map(|s| s.to_string()),
2139            allowed_pattern: None,
2140            key_id: None,
2141            data_type: "text".to_string(),
2142            tier: "Standard".to_string(),
2143            policies: None,
2144            expiration_notified: false,
2145            no_change_notified: false,
2146        };
2147
2148        state.parameters.insert(name.to_string(), parameter);
2149        Ok(ProvisionResult::new(name)
2150            .with("Type", param_type)
2151            .with("Value", value))
2152    }
2153
2154    fn delete_ssm_parameter(&self, physical_id: &str) -> Result<(), String> {
2155        let mut accounts = self.ssm_state.write();
2156        let state = accounts.get_or_create(&self.account_id);
2157        state.parameters.remove(physical_id);
2158        Ok(())
2159    }
2160
2161    // --- IAM Role ---
2162
2163    fn create_iam_role(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
2164        let props = &resource.properties;
2165        let role_name = props
2166            .get("RoleName")
2167            .and_then(|v| v.as_str())
2168            .unwrap_or(&resource.logical_id);
2169
2170        let assume_role_policy = props
2171            .get("AssumeRolePolicyDocument")
2172            .map(|v| {
2173                if v.is_string() {
2174                    v.as_str().unwrap().to_string()
2175                } else {
2176                    serde_json::to_string(v).unwrap_or_default()
2177                }
2178            })
2179            .unwrap_or_default();
2180
2181        let path = props.get("Path").and_then(|v| v.as_str()).unwrap_or("/");
2182
2183        let mut accounts = self.iam_state.write();
2184        let state = accounts.get_or_create(&self.account_id);
2185        let role_id = format!(
2186            "FKIA{}",
2187            &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2188        );
2189        let arn = format!(
2190            "arn:aws:iam::{}:role{}{}",
2191            state.account_id,
2192            if path == "/" { "/" } else { path },
2193            role_name
2194        );
2195
2196        let role = IamRole {
2197            role_name: role_name.to_string(),
2198            role_id: role_id.clone(),
2199            arn: arn.clone(),
2200            path: path.to_string(),
2201            assume_role_policy_document: assume_role_policy,
2202            created_at: Utc::now(),
2203            description: props
2204                .get("Description")
2205                .and_then(|v| v.as_str())
2206                .map(|s| s.to_string()),
2207            max_session_duration: 3600,
2208            tags: Vec::new(),
2209            permissions_boundary: None,
2210        };
2211
2212        state.roles.insert(role_name.to_string(), role);
2213        Ok(ProvisionResult::new(arn.clone())
2214            .with("Arn", arn)
2215            .with("RoleId", role_id))
2216    }
2217
2218    fn delete_iam_role(&self, physical_id: &str) -> Result<(), String> {
2219        let mut accounts = self.iam_state.write();
2220        let state = accounts.get_or_create(&self.account_id);
2221        // physical_id is the ARN; find the role name
2222        let role_name = state
2223            .roles
2224            .iter()
2225            .find(|(_, r)| r.arn == physical_id)
2226            .map(|(name, _)| name.clone());
2227        if let Some(name) = role_name {
2228            state.roles.remove(&name);
2229            state.role_policies.remove(&name);
2230            state.role_inline_policies.remove(&name);
2231        }
2232        Ok(())
2233    }
2234
2235    // --- IAM Policy ---
2236
2237    fn create_iam_policy(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
2238        let props = &resource.properties;
2239        let policy_name = props
2240            .get("PolicyName")
2241            .and_then(|v| v.as_str())
2242            .unwrap_or(&resource.logical_id);
2243
2244        let policy_document = props
2245            .get("PolicyDocument")
2246            .map(|v| {
2247                if v.is_string() {
2248                    v.as_str().unwrap().to_string()
2249                } else {
2250                    serde_json::to_string(v).unwrap_or_default()
2251                }
2252            })
2253            .unwrap_or_default();
2254
2255        let path = props.get("Path").and_then(|v| v.as_str()).unwrap_or("/");
2256
2257        let mut accounts = self.iam_state.write();
2258        let state = accounts.get_or_create(&self.account_id);
2259        let policy_id = format!(
2260            "FSIA{}",
2261            &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2262        );
2263        let arn = format!(
2264            "arn:aws:iam::{}:policy{}{}",
2265            state.account_id,
2266            if path == "/" { "/" } else { path },
2267            policy_name
2268        );
2269
2270        let now = Utc::now();
2271        let policy = IamPolicy {
2272            policy_name: policy_name.to_string(),
2273            policy_id,
2274            arn: arn.clone(),
2275            path: path.to_string(),
2276            description: props
2277                .get("Description")
2278                .and_then(|v| v.as_str())
2279                .unwrap_or("")
2280                .to_string(),
2281            created_at: now,
2282            tags: Vec::new(),
2283            default_version_id: "v1".to_string(),
2284            versions: vec![PolicyVersion {
2285                version_id: "v1".to_string(),
2286                document: policy_document,
2287                is_default: true,
2288                created_at: now,
2289            }],
2290            next_version_num: 2,
2291            attachment_count: 0,
2292        };
2293
2294        state.policies.insert(arn.clone(), policy);
2295        Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
2296    }
2297
2298    fn delete_iam_policy(&self, physical_id: &str) -> Result<(), String> {
2299        let mut accounts = self.iam_state.write();
2300        let state = accounts.get_or_create(&self.account_id);
2301        state.policies.remove(physical_id);
2302        Ok(())
2303    }
2304
2305    fn create_iam_user(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
2306        let props = &resource.properties;
2307        let user_name = props
2308            .get("UserName")
2309            .and_then(|v| v.as_str())
2310            .unwrap_or(&resource.logical_id)
2311            .to_string();
2312        let path = props
2313            .get("Path")
2314            .and_then(|v| v.as_str())
2315            .unwrap_or("/")
2316            .to_string();
2317        let permissions_boundary = props
2318            .get("PermissionsBoundary")
2319            .and_then(|v| v.as_str())
2320            .map(|s| s.to_string());
2321        let tags = parse_iam_tags(props.get("Tags"));
2322
2323        let mut accounts = self.iam_state.write();
2324        let state = accounts.get_or_create(&self.account_id);
2325        if state.users.contains_key(&user_name) {
2326            return Err(format!("User {user_name} already exists"));
2327        }
2328        let arn = format!(
2329            "arn:aws:iam::{}:user{}{}",
2330            state.account_id, path, user_name
2331        );
2332        let user_id = format!(
2333            "AIDA{}",
2334            &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2335        );
2336        let user = IamUser {
2337            user_name: user_name.clone(),
2338            user_id: user_id.clone(),
2339            arn: arn.clone(),
2340            path,
2341            created_at: Utc::now(),
2342            tags,
2343            permissions_boundary,
2344        };
2345        state.users.insert(user_name.clone(), user);
2346
2347        // Inline + managed policies declared inline on the user.
2348        if let Some(policies) = props.get("Policies").and_then(|v| v.as_array()) {
2349            let inline = state
2350                .user_inline_policies
2351                .entry(user_name.clone())
2352                .or_default();
2353            for p in policies {
2354                if let (Some(n), Some(doc)) = (
2355                    p.get("PolicyName").and_then(|v| v.as_str()),
2356                    p.get("PolicyDocument"),
2357                ) {
2358                    let document = if doc.is_string() {
2359                        doc.as_str().unwrap_or("").to_string()
2360                    } else {
2361                        serde_json::to_string(doc).unwrap_or_default()
2362                    };
2363                    inline.insert(n.to_string(), document);
2364                }
2365            }
2366        }
2367        if let Some(arns) = props.get("ManagedPolicyArns").and_then(|v| v.as_array()) {
2368            let attached = state.user_policies.entry(user_name.clone()).or_default();
2369            for a in arns {
2370                if let Some(s) = a.as_str() {
2371                    if !attached.contains(&s.to_string()) {
2372                        attached.push(s.to_string());
2373                    }
2374                }
2375            }
2376        }
2377        if let Some(groups) = props.get("Groups").and_then(|v| v.as_array()) {
2378            for g in groups {
2379                if let Some(g_name) = g.as_str() {
2380                    if let Some(group) = state.groups.get_mut(g_name) {
2381                        if !group.members.iter().any(|m| m == &user_name) {
2382                            group.members.push(user_name.clone());
2383                        }
2384                    }
2385                }
2386            }
2387        }
2388
2389        Ok(ProvisionResult::new(user_name).with("Arn", arn))
2390    }
2391
2392    fn delete_iam_user(&self, physical_id: &str) -> Result<(), String> {
2393        let mut accounts = self.iam_state.write();
2394        let state = accounts.get_or_create(&self.account_id);
2395        state.users.remove(physical_id);
2396        state.user_inline_policies.remove(physical_id);
2397        state.user_policies.remove(physical_id);
2398        state.access_keys.remove(physical_id);
2399        for group in state.groups.values_mut() {
2400            group.members.retain(|m| m != physical_id);
2401        }
2402        Ok(())
2403    }
2404
2405    fn create_iam_group(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
2406        let props = &resource.properties;
2407        let group_name = props
2408            .get("GroupName")
2409            .and_then(|v| v.as_str())
2410            .unwrap_or(&resource.logical_id)
2411            .to_string();
2412        let path = props
2413            .get("Path")
2414            .and_then(|v| v.as_str())
2415            .unwrap_or("/")
2416            .to_string();
2417
2418        let mut accounts = self.iam_state.write();
2419        let state = accounts.get_or_create(&self.account_id);
2420        if state.groups.contains_key(&group_name) {
2421            return Err(format!("Group {group_name} already exists"));
2422        }
2423        let arn = format!(
2424            "arn:aws:iam::{}:group{}{}",
2425            state.account_id, path, group_name
2426        );
2427        let group_id = format!(
2428            "AGPA{}",
2429            &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2430        );
2431        let mut inline_policies: BTreeMap<String, String> = BTreeMap::new();
2432        if let Some(policies) = props.get("Policies").and_then(|v| v.as_array()) {
2433            for p in policies {
2434                if let (Some(n), Some(doc)) = (
2435                    p.get("PolicyName").and_then(|v| v.as_str()),
2436                    p.get("PolicyDocument"),
2437                ) {
2438                    let document = if doc.is_string() {
2439                        doc.as_str().unwrap_or("").to_string()
2440                    } else {
2441                        serde_json::to_string(doc).unwrap_or_default()
2442                    };
2443                    inline_policies.insert(n.to_string(), document);
2444                }
2445            }
2446        }
2447        let mut attached_policies: Vec<String> = Vec::new();
2448        if let Some(arns) = props.get("ManagedPolicyArns").and_then(|v| v.as_array()) {
2449            for a in arns {
2450                if let Some(s) = a.as_str() {
2451                    attached_policies.push(s.to_string());
2452                }
2453            }
2454        }
2455        state.groups.insert(
2456            group_name.clone(),
2457            IamGroup {
2458                group_name: group_name.clone(),
2459                group_id,
2460                arn: arn.clone(),
2461                path,
2462                created_at: Utc::now(),
2463                members: Vec::new(),
2464                inline_policies,
2465                attached_policies,
2466            },
2467        );
2468
2469        Ok(ProvisionResult::new(group_name).with("Arn", arn))
2470    }
2471
2472    fn delete_iam_group(&self, physical_id: &str) -> Result<(), String> {
2473        let mut accounts = self.iam_state.write();
2474        let state = accounts.get_or_create(&self.account_id);
2475        state.groups.remove(physical_id);
2476        Ok(())
2477    }
2478
2479    fn create_iam_managed_policy(
2480        &self,
2481        resource: &ResourceDefinition,
2482    ) -> Result<ProvisionResult, String> {
2483        // Same shape as AWS::IAM::Policy minus the inline-attach knobs;
2484        // ManagedPolicy is a standalone policy, attached separately.
2485        let props = &resource.properties;
2486        let policy_name = props
2487            .get("ManagedPolicyName")
2488            .and_then(|v| v.as_str())
2489            .unwrap_or(&resource.logical_id)
2490            .to_string();
2491        let policy_document = props
2492            .get("PolicyDocument")
2493            .map(|v| {
2494                if v.is_string() {
2495                    v.as_str().unwrap_or("").to_string()
2496                } else {
2497                    serde_json::to_string(v).unwrap_or_default()
2498                }
2499            })
2500            .unwrap_or_default();
2501        let path = props
2502            .get("Path")
2503            .and_then(|v| v.as_str())
2504            .unwrap_or("/")
2505            .to_string();
2506        let description = props
2507            .get("Description")
2508            .and_then(|v| v.as_str())
2509            .unwrap_or("")
2510            .to_string();
2511
2512        let mut accounts = self.iam_state.write();
2513        let state = accounts.get_or_create(&self.account_id);
2514        let arn = format!(
2515            "arn:aws:iam::{}:policy{}{}",
2516            state.account_id,
2517            if path == "/" { "/" } else { path.as_str() },
2518            policy_name
2519        );
2520        if state.policies.contains_key(&arn) {
2521            return Err(format!("Managed policy {policy_name} already exists"));
2522        }
2523        let policy_id = format!(
2524            "ANPA{}",
2525            &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2526        );
2527        let now = Utc::now();
2528        state.policies.insert(
2529            arn.clone(),
2530            IamPolicy {
2531                policy_name,
2532                policy_id,
2533                arn: arn.clone(),
2534                path,
2535                description,
2536                created_at: now,
2537                tags: Vec::new(),
2538                default_version_id: "v1".to_string(),
2539                versions: vec![PolicyVersion {
2540                    version_id: "v1".to_string(),
2541                    document: policy_document,
2542                    is_default: true,
2543                    created_at: now,
2544                }],
2545                next_version_num: 2,
2546                attachment_count: 0,
2547            },
2548        );
2549
2550        // Attach to declared users/groups/roles.
2551        if let Some(users) = props.get("Users").and_then(|v| v.as_array()) {
2552            for u in users {
2553                if let Some(name) = u.as_str() {
2554                    let attached = state.user_policies.entry(name.to_string()).or_default();
2555                    if !attached.contains(&arn) {
2556                        attached.push(arn.clone());
2557                    }
2558                }
2559            }
2560        }
2561        if let Some(groups) = props.get("Groups").and_then(|v| v.as_array()) {
2562            for g in groups {
2563                if let Some(name) = g.as_str() {
2564                    if let Some(group) = state.groups.get_mut(name) {
2565                        if !group.attached_policies.contains(&arn) {
2566                            group.attached_policies.push(arn.clone());
2567                        }
2568                    }
2569                }
2570            }
2571        }
2572        if let Some(roles) = props.get("Roles").and_then(|v| v.as_array()) {
2573            for r in roles {
2574                if let Some(name) = r.as_str() {
2575                    let attached = state.role_policies.entry(name.to_string()).or_default();
2576                    if !attached.contains(&arn) {
2577                        attached.push(arn.clone());
2578                    }
2579                }
2580            }
2581        }
2582
2583        Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
2584    }
2585
2586    fn delete_iam_managed_policy(&self, physical_id: &str) -> Result<(), String> {
2587        let mut accounts = self.iam_state.write();
2588        let state = accounts.get_or_create(&self.account_id);
2589        state.policies.remove(physical_id);
2590        for arns in state.user_policies.values_mut() {
2591            arns.retain(|a| a != physical_id);
2592        }
2593        for arns in state.role_policies.values_mut() {
2594            arns.retain(|a| a != physical_id);
2595        }
2596        for group in state.groups.values_mut() {
2597            group.attached_policies.retain(|a| a != physical_id);
2598        }
2599        Ok(())
2600    }
2601
2602    fn create_iam_user_to_group_addition(
2603        &self,
2604        resource: &ResourceDefinition,
2605    ) -> Result<ProvisionResult, String> {
2606        let props = &resource.properties;
2607        let group_name = props
2608            .get("GroupName")
2609            .and_then(|v| v.as_str())
2610            .ok_or_else(|| "GroupName is required".to_string())?
2611            .to_string();
2612        let users: Vec<String> = props
2613            .get("Users")
2614            .and_then(|v| v.as_array())
2615            .map(|arr| {
2616                arr.iter()
2617                    .filter_map(|u| u.as_str().map(|s| s.to_string()))
2618                    .collect()
2619            })
2620            .unwrap_or_default();
2621
2622        let mut accounts = self.iam_state.write();
2623        let state = accounts.get_or_create(&self.account_id);
2624        let group = state
2625            .groups
2626            .get_mut(&group_name)
2627            .ok_or_else(|| format!("Group {group_name} does not exist"))?;
2628        for u in &users {
2629            if !group.members.iter().any(|m| m == u) {
2630                group.members.push(u.clone());
2631            }
2632        }
2633
2634        // Encode group + users so delete can revert exactly this addition.
2635        let physical_id = format!("{group_name}|{}", users.join(","));
2636        Ok(ProvisionResult::new(physical_id))
2637    }
2638
2639    fn delete_iam_user_to_group_addition(&self, physical_id: &str) -> Result<(), String> {
2640        let mut accounts = self.iam_state.write();
2641        let state = accounts.get_or_create(&self.account_id);
2642        if let Some((group_name, users)) = physical_id.split_once('|') {
2643            if let Some(group) = state.groups.get_mut(group_name) {
2644                let to_remove: Vec<&str> = users.split(',').filter(|s| !s.is_empty()).collect();
2645                group.members.retain(|m| !to_remove.iter().any(|u| u == m));
2646            }
2647        }
2648        Ok(())
2649    }
2650
2651    fn create_iam_access_key(
2652        &self,
2653        resource: &ResourceDefinition,
2654    ) -> Result<ProvisionResult, String> {
2655        let props = &resource.properties;
2656        let user_name = props
2657            .get("UserName")
2658            .and_then(|v| v.as_str())
2659            .ok_or_else(|| "UserName is required".to_string())?
2660            .to_string();
2661        let status = props
2662            .get("Status")
2663            .and_then(|v| v.as_str())
2664            .unwrap_or("Active")
2665            .to_string();
2666
2667        let mut accounts = self.iam_state.write();
2668        let state = accounts.get_or_create(&self.account_id);
2669        if !state.users.contains_key(&user_name) {
2670            return Err(format!("User {user_name} does not exist"));
2671        }
2672        let access_key_id = format!(
2673            "AKIA{}",
2674            &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2675        );
2676        let secret_access_key: String = Uuid::new_v4()
2677            .to_string()
2678            .replace('-', "")
2679            .chars()
2680            .take(40)
2681            .collect();
2682        state
2683            .access_keys
2684            .entry(user_name.clone())
2685            .or_default()
2686            .push(IamAccessKey {
2687                access_key_id: access_key_id.clone(),
2688                secret_access_key: secret_access_key.clone(),
2689                user_name: user_name.clone(),
2690                status,
2691                created_at: Utc::now(),
2692            });
2693
2694        Ok(ProvisionResult::new(access_key_id.clone()).with("SecretAccessKey", secret_access_key))
2695    }
2696
2697    fn delete_iam_access_key(&self, physical_id: &str) -> Result<(), String> {
2698        let mut accounts = self.iam_state.write();
2699        let state = accounts.get_or_create(&self.account_id);
2700        for keys in state.access_keys.values_mut() {
2701            keys.retain(|k| k.access_key_id != physical_id);
2702        }
2703        Ok(())
2704    }
2705
2706    fn create_iam_instance_profile(
2707        &self,
2708        resource: &ResourceDefinition,
2709    ) -> Result<ProvisionResult, String> {
2710        let props = &resource.properties;
2711        let name = props
2712            .get("InstanceProfileName")
2713            .and_then(|v| v.as_str())
2714            .unwrap_or(&resource.logical_id)
2715            .to_string();
2716        let path = props
2717            .get("Path")
2718            .and_then(|v| v.as_str())
2719            .unwrap_or("/")
2720            .to_string();
2721        // Roles[] entries can be plain role names or `Ref`-resolved role
2722        // ARNs (which the IAM Role provisioner emits as physical_id);
2723        // extract the trailing name segment so DescribeInstanceProfile
2724        // round-trips a name list.
2725        let roles: Vec<String> = props
2726            .get("Roles")
2727            .and_then(|v| v.as_array())
2728            .map(|arr| {
2729                arr.iter()
2730                    .filter_map(|r| r.as_str())
2731                    .map(|s| {
2732                        if let Some(rest) = s.strip_prefix("arn:aws:iam::") {
2733                            rest.split(":role/")
2734                                .nth(1)
2735                                .map(|name| name.to_string())
2736                                .unwrap_or_else(|| s.to_string())
2737                        } else {
2738                            s.to_string()
2739                        }
2740                    })
2741                    .collect()
2742            })
2743            .unwrap_or_default();
2744
2745        let mut accounts = self.iam_state.write();
2746        let state = accounts.get_or_create(&self.account_id);
2747        if state.instance_profiles.contains_key(&name) {
2748            return Err(format!("InstanceProfile {name} already exists"));
2749        }
2750        // Force a retry pass when role refs haven't been resolved yet: a
2751        // logical-id placeholder won't match any real role, and silently
2752        // storing it would leave DescribeInstanceProfile returning an
2753        // empty Roles array.
2754        for role_name in &roles {
2755            if !state.roles.contains_key(role_name) {
2756                return Err(format!(
2757                    "InstanceProfile {name}: referenced role {role_name} not yet provisioned"
2758                ));
2759            }
2760        }
2761        let arn = format!(
2762            "arn:aws:iam::{}:instance-profile{}{}",
2763            state.account_id, path, name
2764        );
2765        let id = format!(
2766            "AIPA{}",
2767            &Uuid::new_v4().to_string().replace('-', "").to_uppercase()[..16]
2768        );
2769        state.instance_profiles.insert(
2770            name.clone(),
2771            IamInstanceProfile {
2772                instance_profile_name: name.clone(),
2773                instance_profile_id: id,
2774                arn: arn.clone(),
2775                path,
2776                created_at: Utc::now(),
2777                roles,
2778                tags: Vec::new(),
2779            },
2780        );
2781
2782        Ok(ProvisionResult::new(name).with("Arn", arn))
2783    }
2784
2785    fn delete_iam_instance_profile(&self, physical_id: &str) -> Result<(), String> {
2786        let mut accounts = self.iam_state.write();
2787        let state = accounts.get_or_create(&self.account_id);
2788        state.instance_profiles.remove(physical_id);
2789        Ok(())
2790    }
2791
2792    fn create_iam_oidc_provider(
2793        &self,
2794        resource: &ResourceDefinition,
2795    ) -> Result<ProvisionResult, String> {
2796        let props = &resource.properties;
2797        let url = props
2798            .get("Url")
2799            .and_then(|v| v.as_str())
2800            .ok_or("Url is required")?
2801            .to_string();
2802        let client_id_list: Vec<String> = props
2803            .get("ClientIdList")
2804            .and_then(|v| v.as_array())
2805            .map(|arr| {
2806                arr.iter()
2807                    .filter_map(|v| v.as_str().map(String::from))
2808                    .collect()
2809            })
2810            .unwrap_or_default();
2811        let thumbprint_list: Vec<String> = props
2812            .get("ThumbprintList")
2813            .and_then(|v| v.as_array())
2814            .map(|arr| {
2815                arr.iter()
2816                    .filter_map(|v| v.as_str().map(String::from))
2817                    .collect()
2818            })
2819            .unwrap_or_default();
2820        // Real AWS strips the scheme to form the resource path component.
2821        let url_path = url
2822            .trim_start_matches("https://")
2823            .trim_start_matches("http://")
2824            .to_string();
2825        let arn = format!(
2826            "arn:aws:iam::{}:oidc-provider/{}",
2827            self.account_id, url_path
2828        );
2829        let provider = OidcProvider {
2830            arn: arn.clone(),
2831            url,
2832            client_id_list,
2833            thumbprint_list,
2834            created_at: Utc::now(),
2835            tags: Vec::new(),
2836        };
2837        let mut accounts = self.iam_state.write();
2838        let state = accounts.get_or_create(&self.account_id);
2839        state.oidc_providers.insert(arn.clone(), provider);
2840        Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
2841    }
2842
2843    fn delete_iam_oidc_provider(&self, physical_id: &str) -> Result<(), String> {
2844        let mut accounts = self.iam_state.write();
2845        let state = accounts.get_or_create(&self.account_id);
2846        state.oidc_providers.remove(physical_id);
2847        Ok(())
2848    }
2849
2850    fn create_iam_saml_provider(
2851        &self,
2852        resource: &ResourceDefinition,
2853    ) -> Result<ProvisionResult, String> {
2854        let props = &resource.properties;
2855        let name = props
2856            .get("Name")
2857            .and_then(|v| v.as_str())
2858            .map(String::from)
2859            .unwrap_or_else(|| {
2860                let suffix = Uuid::new_v4().simple().to_string();
2861                format!("{}-{}", resource.logical_id, &suffix[..8])
2862            });
2863        let saml_metadata_document = props
2864            .get("SamlMetadataDocument")
2865            .and_then(|v| v.as_str())
2866            .ok_or("SamlMetadataDocument is required")?
2867            .to_string();
2868        let arn = format!("arn:aws:iam::{}:saml-provider/{name}", self.account_id);
2869        let now = Utc::now();
2870        let valid_until = now + chrono::Duration::days(365 * 10);
2871        let provider = SamlProvider {
2872            arn: arn.clone(),
2873            name,
2874            saml_metadata_document,
2875            created_at: now,
2876            valid_until,
2877            tags: Vec::new(),
2878        };
2879        let mut accounts = self.iam_state.write();
2880        let state = accounts.get_or_create(&self.account_id);
2881        state.saml_providers.insert(arn.clone(), provider);
2882        Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
2883    }
2884
2885    fn delete_iam_saml_provider(&self, physical_id: &str) -> Result<(), String> {
2886        let mut accounts = self.iam_state.write();
2887        let state = accounts.get_or_create(&self.account_id);
2888        state.saml_providers.remove(physical_id);
2889        Ok(())
2890    }
2891
2892    fn create_iam_service_linked_role(
2893        &self,
2894        resource: &ResourceDefinition,
2895    ) -> Result<ProvisionResult, String> {
2896        let props = &resource.properties;
2897        let aws_service_name = props
2898            .get("AWSServiceName")
2899            .and_then(|v| v.as_str())
2900            .ok_or("AWSServiceName is required")?
2901            .to_string();
2902        let custom_suffix = props
2903            .get("CustomSuffix")
2904            .and_then(|v| v.as_str())
2905            .map(String::from);
2906        let description = props
2907            .get("Description")
2908            .and_then(|v| v.as_str())
2909            .unwrap_or("")
2910            .to_string();
2911        // AWS service-linked role naming convention.
2912        let service_short = aws_service_name.split('.').next().unwrap_or("Service");
2913        let role_name = match &custom_suffix {
2914            Some(s) => format!("AWSServiceRoleFor{service_short}_{s}"),
2915            None => format!("AWSServiceRoleFor{service_short}"),
2916        };
2917        let path = format!("/aws-service-role/{aws_service_name}/");
2918        let arn = format!("arn:aws:iam::{}:role{path}{role_name}", self.account_id);
2919        // Service-linked roles get a trust policy specific to the service.
2920        let assume_role_policy_document = serde_json::json!({
2921            "Version": "2012-10-17",
2922            "Statement": [{
2923                "Effect": "Allow",
2924                "Principal": {"Service": aws_service_name.clone()},
2925                "Action": "sts:AssumeRole"
2926            }]
2927        })
2928        .to_string();
2929        let role_id_suffix = Uuid::new_v4().simple().to_string();
2930        let role = IamRole {
2931            role_name: role_name.clone(),
2932            role_id: format!("AROA{}", role_id_suffix[..16].to_uppercase()),
2933            arn: arn.clone(),
2934            path,
2935            assume_role_policy_document,
2936            created_at: Utc::now(),
2937            description: Some(description),
2938            max_session_duration: 3600,
2939            tags: Vec::new(),
2940            permissions_boundary: None,
2941        };
2942        let mut accounts = self.iam_state.write();
2943        let state = accounts.get_or_create(&self.account_id);
2944        state.roles.insert(role_name.clone(), role);
2945        Ok(ProvisionResult::new(role_name)
2946            .with("Arn", arn)
2947            .with("RoleId", String::new()))
2948    }
2949
2950    fn delete_iam_service_linked_role(&self, physical_id: &str) -> Result<(), String> {
2951        let mut accounts = self.iam_state.write();
2952        let state = accounts.get_or_create(&self.account_id);
2953        state.roles.remove(physical_id);
2954        Ok(())
2955    }
2956
2957    fn create_iam_virtual_mfa_device(
2958        &self,
2959        resource: &ResourceDefinition,
2960    ) -> Result<ProvisionResult, String> {
2961        let props = &resource.properties;
2962        let name = props
2963            .get("VirtualMfaDeviceName")
2964            .and_then(|v| v.as_str())
2965            .ok_or("VirtualMfaDeviceName is required")?
2966            .to_string();
2967        let path = props
2968            .get("Path")
2969            .and_then(|v| v.as_str())
2970            .unwrap_or("/")
2971            .to_string();
2972        let serial_number = format!("arn:aws:iam::{}:mfa{}{name}", self.account_id, path);
2973        // Real AWS returns a base32 seed + a PNG QR code; we synthesize
2974        // deterministic placeholders so callers can read them back.
2975        let seed = format!("BASE32SEED{}", Uuid::new_v4().simple());
2976        let user = props
2977            .get("Users")
2978            .and_then(|v| v.as_array())
2979            .and_then(|arr| arr.first())
2980            .and_then(|v| v.as_str())
2981            .map(String::from);
2982        let device = VirtualMfaDevice {
2983            serial_number: serial_number.clone(),
2984            base32_string_seed: seed,
2985            qr_code_png: String::new(),
2986            enable_date: user.as_ref().map(|_| Utc::now()),
2987            user,
2988            tags: Vec::new(),
2989        };
2990        let mut accounts = self.iam_state.write();
2991        let state = accounts.get_or_create(&self.account_id);
2992        state
2993            .virtual_mfa_devices
2994            .insert(serial_number.clone(), device);
2995        Ok(ProvisionResult::new(serial_number.clone()).with("SerialNumber", serial_number))
2996    }
2997
2998    fn delete_iam_virtual_mfa_device(&self, physical_id: &str) -> Result<(), String> {
2999        let mut accounts = self.iam_state.write();
3000        let state = accounts.get_or_create(&self.account_id);
3001        state.virtual_mfa_devices.remove(physical_id);
3002        Ok(())
3003    }
3004
3005    // --- S3 ---
3006
3007    fn create_s3_bucket(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
3008        let props = &resource.properties;
3009        let bucket_name = props
3010            .get("BucketName")
3011            .and_then(|v| v.as_str())
3012            .unwrap_or(&resource.logical_id);
3013
3014        let mut __s3_mas = self.s3_state.write();
3015        let state = __s3_mas.get_or_create(&self.account_id);
3016        let region = state.region.clone();
3017        let bucket = S3Bucket::new(bucket_name, &state.region, &state.account_id);
3018        state.buckets.insert(bucket_name.to_string(), bucket);
3019
3020        let arn = format!("arn:aws:s3:::{bucket_name}");
3021        let domain_name = format!("{bucket_name}.s3.amazonaws.com");
3022        let regional_domain_name = format!("{bucket_name}.s3.{region}.amazonaws.com");
3023        let dual_stack_domain_name = format!("{bucket_name}.s3.dualstack.{region}.amazonaws.com");
3024        let website_url = format!("http://{bucket_name}.s3-website-{region}.amazonaws.com");
3025        Ok(ProvisionResult::new(bucket_name)
3026            .with("Arn", arn)
3027            .with("DomainName", domain_name)
3028            .with("RegionalDomainName", regional_domain_name)
3029            .with("DualStackDomainName", dual_stack_domain_name)
3030            .with("WebsiteURL", website_url))
3031    }
3032
3033    fn delete_s3_bucket(&self, physical_id: &str) -> Result<(), String> {
3034        let mut __s3_mas = self.s3_state.write();
3035        let state = __s3_mas.get_or_create(&self.account_id);
3036        state.buckets.remove(physical_id);
3037        Ok(())
3038    }
3039
3040    // --- EventBridge ---
3041
3042    fn create_eventbridge_rule(
3043        &self,
3044        resource: &ResourceDefinition,
3045    ) -> Result<ProvisionResult, String> {
3046        let props = &resource.properties;
3047        let rule_name = props
3048            .get("Name")
3049            .and_then(|v| v.as_str())
3050            .unwrap_or(&resource.logical_id);
3051        let event_bus_name = props
3052            .get("EventBusName")
3053            .and_then(|v| v.as_str())
3054            .unwrap_or("default");
3055
3056        let mut eb_accounts = self.eventbridge_state.write();
3057        let state = eb_accounts.get_or_create(&self.account_id);
3058
3059        // Validate that the event bus exists
3060        if !state.buses.contains_key(event_bus_name) {
3061            return Err(format!("Event bus does not exist: {event_bus_name}"));
3062        }
3063
3064        let arn = if event_bus_name == "default" {
3065            format!(
3066                "arn:aws:events:{}:{}:rule/{}",
3067                state.region, state.account_id, rule_name
3068            )
3069        } else {
3070            format!(
3071                "arn:aws:events:{}:{}:rule/{}/{}",
3072                state.region, state.account_id, event_bus_name, rule_name
3073            )
3074        };
3075
3076        let rule = EventRule {
3077            name: rule_name.to_string(),
3078            arn: arn.clone(),
3079            event_bus_name: event_bus_name.to_string(),
3080            event_pattern: props.get("EventPattern").map(|v| {
3081                if v.is_string() {
3082                    v.as_str().unwrap().to_string()
3083                } else {
3084                    serde_json::to_string(v).unwrap_or_default()
3085                }
3086            }),
3087            schedule_expression: props
3088                .get("ScheduleExpression")
3089                .and_then(|v| v.as_str())
3090                .map(|s| s.to_string()),
3091            state: props
3092                .get("State")
3093                .and_then(|v| v.as_str())
3094                .unwrap_or("ENABLED")
3095                .to_string(),
3096            description: props
3097                .get("Description")
3098                .and_then(|v| v.as_str())
3099                .map(|s| s.to_string()),
3100            role_arn: props
3101                .get("RoleArn")
3102                .and_then(|v| v.as_str())
3103                .map(|s| s.to_string()),
3104            managed_by: None,
3105            created_by: None,
3106            targets: Vec::new(),
3107            tags: std::collections::BTreeMap::new(),
3108            last_fired: None,
3109        };
3110
3111        state
3112            .rules
3113            .insert((event_bus_name.to_string(), rule_name.to_string()), rule);
3114        Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
3115    }
3116
3117    fn delete_eventbridge_rule(&self, physical_id: &str) -> Result<(), String> {
3118        let mut eb_accounts = self.eventbridge_state.write();
3119        let state = eb_accounts.default_mut();
3120        // physical_id is the ARN; find the rule key
3121        let key = state
3122            .rules
3123            .iter()
3124            .find(|(_, r)| r.arn == physical_id)
3125            .map(|(k, _)| k.clone());
3126        if let Some(k) = key {
3127            state.rules.remove(&k);
3128        }
3129        Ok(())
3130    }
3131
3132    fn create_eventbridge_connection(
3133        &self,
3134        resource: &ResourceDefinition,
3135    ) -> Result<ProvisionResult, String> {
3136        let props = &resource.properties;
3137        let name = props
3138            .get("Name")
3139            .and_then(|v| v.as_str())
3140            .unwrap_or(&resource.logical_id)
3141            .to_string();
3142        let description = props
3143            .get("Description")
3144            .and_then(|v| v.as_str())
3145            .map(|s| s.to_string());
3146        let authorization_type = props
3147            .get("AuthorizationType")
3148            .and_then(|v| v.as_str())
3149            .unwrap_or("API_KEY")
3150            .to_string();
3151        let auth_parameters = props
3152            .get("AuthParameters")
3153            .cloned()
3154            .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
3155
3156        let mut eb_accounts = self.eventbridge_state.write();
3157        let state = eb_accounts.get_or_create(&self.account_id);
3158        if state.connections.contains_key(&name) {
3159            return Err(format!("Connection {name} already exists"));
3160        }
3161        let now = Utc::now();
3162        let arn = format!(
3163            "arn:aws:events:{}:{}:connection/{}/{}",
3164            state.region,
3165            state.account_id,
3166            name,
3167            Uuid::new_v4().as_simple()
3168        );
3169        let secret_arn = format!(
3170            "arn:aws:secretsmanager:{}:{}:secret:events!connection/{}-{}",
3171            state.region,
3172            state.account_id,
3173            name,
3174            Uuid::new_v4().as_simple()
3175        );
3176        let connection = Connection {
3177            name: name.clone(),
3178            arn: arn.clone(),
3179            description,
3180            authorization_type,
3181            auth_parameters,
3182            connection_state: "AUTHORIZED".to_string(),
3183            secret_arn: secret_arn.clone(),
3184            creation_time: now,
3185            last_modified_time: now,
3186            last_authorized_time: now,
3187        };
3188        state.connections.insert(name.clone(), connection);
3189
3190        Ok(ProvisionResult::new(name)
3191            .with("Arn", arn)
3192            .with("SecretArn", secret_arn))
3193    }
3194
3195    fn delete_eventbridge_connection(&self, physical_id: &str) -> Result<(), String> {
3196        let mut eb_accounts = self.eventbridge_state.write();
3197        let state = eb_accounts.get_or_create(&self.account_id);
3198        state.connections.remove(physical_id);
3199        Ok(())
3200    }
3201
3202    fn create_eventbridge_api_destination(
3203        &self,
3204        resource: &ResourceDefinition,
3205    ) -> Result<ProvisionResult, String> {
3206        let props = &resource.properties;
3207        let name = props
3208            .get("Name")
3209            .and_then(|v| v.as_str())
3210            .unwrap_or(&resource.logical_id)
3211            .to_string();
3212        let description = props
3213            .get("Description")
3214            .and_then(|v| v.as_str())
3215            .map(|s| s.to_string());
3216        let connection_arn = props
3217            .get("ConnectionArn")
3218            .and_then(|v| v.as_str())
3219            .ok_or_else(|| "ConnectionArn is required".to_string())?
3220            .to_string();
3221        let invocation_endpoint = props
3222            .get("InvocationEndpoint")
3223            .and_then(|v| v.as_str())
3224            .ok_or_else(|| "InvocationEndpoint is required".to_string())?
3225            .to_string();
3226        let http_method = props
3227            .get("HttpMethod")
3228            .and_then(|v| v.as_str())
3229            .unwrap_or("POST")
3230            .to_string();
3231        let invocation_rate_limit_per_second = props
3232            .get("InvocationRateLimitPerSecond")
3233            .and_then(|v| v.as_i64());
3234
3235        let mut eb_accounts = self.eventbridge_state.write();
3236        let state = eb_accounts.get_or_create(&self.account_id);
3237        if state.api_destinations.contains_key(&name) {
3238            return Err(format!("ApiDestination {name} already exists"));
3239        }
3240        let now = Utc::now();
3241        let arn = format!(
3242            "arn:aws:events:{}:{}:api-destination/{}/{}",
3243            state.region,
3244            state.account_id,
3245            name,
3246            Uuid::new_v4().as_simple()
3247        );
3248        state.api_destinations.insert(
3249            name.clone(),
3250            ApiDestination {
3251                name: name.clone(),
3252                arn: arn.clone(),
3253                description,
3254                connection_arn,
3255                invocation_endpoint,
3256                http_method,
3257                invocation_rate_limit_per_second,
3258                state: "ACTIVE".to_string(),
3259                creation_time: now,
3260                last_modified_time: now,
3261            },
3262        );
3263
3264        Ok(ProvisionResult::new(name).with("Arn", arn))
3265    }
3266
3267    fn delete_eventbridge_api_destination(&self, physical_id: &str) -> Result<(), String> {
3268        let mut eb_accounts = self.eventbridge_state.write();
3269        let state = eb_accounts.get_or_create(&self.account_id);
3270        state.api_destinations.remove(physical_id);
3271        Ok(())
3272    }
3273
3274    fn create_eventbridge_archive(
3275        &self,
3276        resource: &ResourceDefinition,
3277    ) -> Result<ProvisionResult, String> {
3278        let props = &resource.properties;
3279        let name = props
3280            .get("ArchiveName")
3281            .and_then(|v| v.as_str())
3282            .unwrap_or(&resource.logical_id)
3283            .to_string();
3284        let event_source_arn = props
3285            .get("SourceArn")
3286            .and_then(|v| v.as_str())
3287            .ok_or_else(|| "SourceArn is required".to_string())?
3288            .to_string();
3289        let description = props
3290            .get("Description")
3291            .and_then(|v| v.as_str())
3292            .map(|s| s.to_string());
3293        let event_pattern = props.get("EventPattern").map(|v| {
3294            if v.is_string() {
3295                v.as_str().unwrap_or("").to_string()
3296            } else {
3297                serde_json::to_string(v).unwrap_or_default()
3298            }
3299        });
3300        let retention_days = props
3301            .get("RetentionDays")
3302            .and_then(|v| v.as_i64())
3303            .unwrap_or(0);
3304
3305        let mut eb_accounts = self.eventbridge_state.write();
3306        let state = eb_accounts.get_or_create(&self.account_id);
3307        if state.archives.contains_key(&name) {
3308            return Err(format!("Archive {name} already exists"));
3309        }
3310        let arn = format!(
3311            "arn:aws:events:{}:{}:archive/{}",
3312            state.region, state.account_id, name
3313        );
3314        state.archives.insert(
3315            name.clone(),
3316            Archive {
3317                name: name.clone(),
3318                arn: arn.clone(),
3319                event_source_arn,
3320                description,
3321                event_pattern,
3322                retention_days,
3323                state: "ENABLED".to_string(),
3324                creation_time: Utc::now(),
3325                event_count: 0,
3326                size_bytes: 0,
3327                events: Vec::new(),
3328            },
3329        );
3330
3331        Ok(ProvisionResult::new(name).with("Arn", arn))
3332    }
3333
3334    fn delete_eventbridge_archive(&self, physical_id: &str) -> Result<(), String> {
3335        let mut eb_accounts = self.eventbridge_state.write();
3336        let state = eb_accounts.get_or_create(&self.account_id);
3337        state.archives.remove(physical_id);
3338        Ok(())
3339    }
3340
3341    fn create_eventbridge_event_bus(
3342        &self,
3343        resource: &ResourceDefinition,
3344    ) -> Result<ProvisionResult, String> {
3345        let props = &resource.properties;
3346        let name = props
3347            .get("Name")
3348            .and_then(|v| v.as_str())
3349            .ok_or("Name is required")?
3350            .to_string();
3351        let description = props
3352            .get("Description")
3353            .and_then(|v| v.as_str())
3354            .map(String::from);
3355        let kms_key_identifier = props
3356            .get("KmsKeyIdentifier")
3357            .and_then(|v| v.as_str())
3358            .map(String::from);
3359        let dead_letter_config = props.get("DeadLetterConfig").cloned();
3360        let policy = props.get("Policy").cloned();
3361        let arn = format!(
3362            "arn:aws:events:{}:{}:event-bus/{name}",
3363            self.region, self.account_id
3364        );
3365        let now = Utc::now();
3366        let bus = EventBus {
3367            name: name.clone(),
3368            arn: arn.clone(),
3369            tags: BTreeMap::new(),
3370            policy,
3371            description,
3372            kms_key_identifier,
3373            dead_letter_config,
3374            creation_time: now,
3375            last_modified_time: now,
3376        };
3377
3378        let mut eb_accounts = self.eventbridge_state.write();
3379        let state = eb_accounts.get_or_create(&self.account_id);
3380        state.buses.insert(name.clone(), bus);
3381
3382        Ok(ProvisionResult::new(name).with("Arn", arn))
3383    }
3384
3385    fn delete_eventbridge_event_bus(&self, physical_id: &str) -> Result<(), String> {
3386        let mut eb_accounts = self.eventbridge_state.write();
3387        let state = eb_accounts.get_or_create(&self.account_id);
3388        // The default bus is reserved; refuse to delete it.
3389        if physical_id == "default" {
3390            return Ok(());
3391        }
3392        state.buses.remove(physical_id);
3393        Ok(())
3394    }
3395
3396    fn create_eventbridge_event_bus_policy(
3397        &self,
3398        resource: &ResourceDefinition,
3399    ) -> Result<ProvisionResult, String> {
3400        let props = &resource.properties;
3401        let bus_name = props
3402            .get("EventBusName")
3403            .and_then(|v| v.as_str())
3404            .unwrap_or("default")
3405            .to_string();
3406        // Statement is the v2 shape; the older v1 shape has Action+Principal+
3407        // StatementId; both ultimately end up as a single statement on the bus
3408        // policy. We accept either form.
3409        let statement = if let Some(s) = props.get("Statement") {
3410            s.clone()
3411        } else {
3412            let sid = props
3413                .get("Sid")
3414                .or_else(|| props.get("StatementId"))
3415                .and_then(|v| v.as_str())
3416                .map(String::from);
3417            let action = props
3418                .get("Action")
3419                .and_then(|v| v.as_str())
3420                .map(String::from);
3421            let principal = props.get("Principal").cloned();
3422            let condition = props.get("Condition").cloned();
3423            let mut obj = serde_json::json!({
3424                "Effect": "Allow",
3425                "Resource": format!(
3426                    "arn:aws:events:{}:{}:event-bus/{bus_name}",
3427                    self.region, self.account_id
3428                ),
3429            });
3430            if let (Some(sid), Some(obj)) = (sid, obj.as_object_mut()) {
3431                obj.insert("Sid".to_string(), serde_json::Value::String(sid));
3432            }
3433            if let (Some(action), Some(obj)) = (action, obj.as_object_mut()) {
3434                obj.insert("Action".to_string(), serde_json::Value::String(action));
3435            }
3436            if let (Some(principal), Some(obj)) = (principal, obj.as_object_mut()) {
3437                obj.insert("Principal".to_string(), principal);
3438            }
3439            if let (Some(condition), Some(obj)) = (condition, obj.as_object_mut()) {
3440                obj.insert("Condition".to_string(), condition);
3441            }
3442            obj
3443        };
3444
3445        let mut eb_accounts = self.eventbridge_state.write();
3446        let state = eb_accounts.get_or_create(&self.account_id);
3447        let bus = state
3448            .buses
3449            .get_mut(&bus_name)
3450            .ok_or_else(|| format!("EventBus {bus_name} not yet provisioned"))?;
3451        // Append to the existing policy's Statement array, or create one.
3452        match bus.policy.as_mut() {
3453            Some(serde_json::Value::Object(obj)) => {
3454                if let Some(serde_json::Value::Array(arr)) = obj.get_mut("Statement") {
3455                    arr.push(statement);
3456                } else {
3457                    obj.insert(
3458                        "Statement".to_string(),
3459                        serde_json::Value::Array(vec![statement]),
3460                    );
3461                }
3462            }
3463            _ => {
3464                bus.policy = Some(serde_json::json!({
3465                    "Version": "2012-10-17",
3466                    "Statement": [statement],
3467                }));
3468            }
3469        }
3470
3471        // Physical id encodes the bus name + a synthetic id so delete locates the
3472        // entry even when the user never sets Sid/StatementId.
3473        let pid = format!("{bus_name}|{}", Uuid::new_v4().simple());
3474        Ok(ProvisionResult::new(pid))
3475    }
3476
3477    fn delete_eventbridge_event_bus_policy(&self, physical_id: &str) -> Result<(), String> {
3478        let bus_name = physical_id
3479            .split_once('|')
3480            .map(|(b, _)| b.to_string())
3481            .unwrap_or_else(|| "default".to_string());
3482        let mut eb_accounts = self.eventbridge_state.write();
3483        let state = eb_accounts.get_or_create(&self.account_id);
3484        if let Some(bus) = state.buses.get_mut(&bus_name) {
3485            bus.policy = None;
3486        }
3487        Ok(())
3488    }
3489
3490    fn create_eventbridge_endpoint(
3491        &self,
3492        resource: &ResourceDefinition,
3493    ) -> Result<ProvisionResult, String> {
3494        let props = &resource.properties;
3495        let name = props
3496            .get("Name")
3497            .and_then(|v| v.as_str())
3498            .ok_or("Name is required")?
3499            .to_string();
3500        let description = props
3501            .get("Description")
3502            .and_then(|v| v.as_str())
3503            .map(String::from);
3504        let routing_config = props
3505            .get("RoutingConfig")
3506            .cloned()
3507            .ok_or("RoutingConfig is required")?;
3508        let replication_config = props.get("ReplicationConfig").cloned();
3509        let event_buses = props
3510            .get("EventBuses")
3511            .and_then(|v| v.as_array())
3512            .cloned()
3513            .unwrap_or_default();
3514        let role_arn = props
3515            .get("RoleArn")
3516            .and_then(|v| v.as_str())
3517            .map(String::from);
3518
3519        let endpoint_id = Uuid::new_v4().simple().to_string()[..16].to_string();
3520        let arn = format!(
3521            "arn:aws:events:{}:{}:endpoint/{name}",
3522            self.region, self.account_id
3523        );
3524        let endpoint_url = format!("https://{endpoint_id}.endpoint.events.amazonaws.com");
3525        let now = Utc::now();
3526        let endpoint = Endpoint {
3527            name: name.clone(),
3528            arn: arn.clone(),
3529            endpoint_id: endpoint_id.clone(),
3530            endpoint_url: Some(endpoint_url.clone()),
3531            description,
3532            routing_config,
3533            replication_config,
3534            event_buses,
3535            role_arn,
3536            state: "ACTIVE".to_string(),
3537            creation_time: now,
3538            last_modified_time: now,
3539        };
3540
3541        let mut eb_accounts = self.eventbridge_state.write();
3542        let state = eb_accounts.get_or_create(&self.account_id);
3543        state.endpoints.insert(name.clone(), endpoint);
3544
3545        Ok(ProvisionResult::new(name)
3546            .with("Arn", arn)
3547            .with("EndpointId", endpoint_id)
3548            .with("EndpointUrl", endpoint_url)
3549            .with("State", "ACTIVE"))
3550    }
3551
3552    fn delete_eventbridge_endpoint(&self, physical_id: &str) -> Result<(), String> {
3553        let mut eb_accounts = self.eventbridge_state.write();
3554        let state = eb_accounts.get_or_create(&self.account_id);
3555        state.endpoints.remove(physical_id);
3556        Ok(())
3557    }
3558
3559    // --- DynamoDB ---
3560
3561    fn create_dynamodb_table(
3562        &self,
3563        resource: &ResourceDefinition,
3564    ) -> Result<ProvisionResult, String> {
3565        let props = &resource.properties;
3566        let table_name = props
3567            .get("TableName")
3568            .and_then(|v| v.as_str())
3569            .unwrap_or(&resource.logical_id);
3570
3571        let mut key_schema = Vec::new();
3572        if let Some(ks) = props.get("KeySchema").and_then(|v| v.as_array()) {
3573            for item in ks {
3574                let attr_name = item
3575                    .get("AttributeName")
3576                    .and_then(|v| v.as_str())
3577                    .unwrap_or("")
3578                    .to_string();
3579                let key_type = item
3580                    .get("KeyType")
3581                    .and_then(|v| v.as_str())
3582                    .unwrap_or("HASH")
3583                    .to_string();
3584                key_schema.push(KeySchemaElement {
3585                    attribute_name: attr_name,
3586                    key_type,
3587                });
3588            }
3589        }
3590
3591        let mut attribute_definitions = Vec::new();
3592        if let Some(defs) = props.get("AttributeDefinitions").and_then(|v| v.as_array()) {
3593            for item in defs {
3594                let attr_name = item
3595                    .get("AttributeName")
3596                    .and_then(|v| v.as_str())
3597                    .unwrap_or("")
3598                    .to_string();
3599                let attr_type = item
3600                    .get("AttributeType")
3601                    .and_then(|v| v.as_str())
3602                    .unwrap_or("S")
3603                    .to_string();
3604                attribute_definitions.push(AttributeDefinition {
3605                    attribute_name: attr_name,
3606                    attribute_type: attr_type,
3607                });
3608            }
3609        }
3610
3611        let billing_mode = props
3612            .get("BillingMode")
3613            .and_then(|v| v.as_str())
3614            .unwrap_or("PAY_PER_REQUEST")
3615            .to_string();
3616
3617        let provisioned_throughput = if billing_mode == "PROVISIONED" {
3618            if let Some(pt) = props.get("ProvisionedThroughput") {
3619                ProvisionedThroughput {
3620                    read_capacity_units: pt
3621                        .get("ReadCapacityUnits")
3622                        .and_then(|v| v.as_i64())
3623                        .unwrap_or(5),
3624                    write_capacity_units: pt
3625                        .get("WriteCapacityUnits")
3626                        .and_then(|v| v.as_i64())
3627                        .unwrap_or(5),
3628                }
3629            } else {
3630                ProvisionedThroughput {
3631                    read_capacity_units: 5,
3632                    write_capacity_units: 5,
3633                }
3634            }
3635        } else {
3636            ProvisionedThroughput {
3637                read_capacity_units: 0,
3638                write_capacity_units: 0,
3639            }
3640        };
3641
3642        // Parse StreamSpecification from CloudFormation properties
3643        let (stream_enabled, stream_view_type) =
3644            if let Some(stream_spec) = props.get("StreamSpecification") {
3645                let view_type = stream_spec
3646                    .get("StreamViewType")
3647                    .and_then(|v| v.as_str())
3648                    .map(|s| s.to_string());
3649                let enabled = stream_spec
3650                    .get("StreamEnabled")
3651                    .and_then(|v| v.as_bool().or_else(|| v.as_str().map(|s| s == "true")))
3652                    // If StreamViewType is set, treat streams as enabled even if StreamEnabled is missing
3653                    .unwrap_or(view_type.is_some());
3654                (enabled, view_type)
3655            } else {
3656                (false, None)
3657            };
3658
3659        let deletion_protection_enabled = props
3660            .get("DeletionProtectionEnabled")
3661            .and_then(|v| v.as_bool().or_else(|| v.as_str().map(|s| s == "true")))
3662            .unwrap_or(false);
3663
3664        let on_demand_throughput = props
3665            .get("OnDemandThroughput")
3666            .map(|odt| OnDemandThroughput {
3667                max_read_request_units: odt
3668                    .get("MaxReadRequestUnits")
3669                    .and_then(|v| v.as_i64())
3670                    .unwrap_or(-1),
3671                max_write_request_units: odt
3672                    .get("MaxWriteRequestUnits")
3673                    .and_then(|v| v.as_i64())
3674                    .unwrap_or(-1),
3675            });
3676
3677        let mut __ddb_mas = self.dynamodb_state.write();
3678        let state = __ddb_mas.get_or_create(&self.account_id);
3679        let arn = format!(
3680            "arn:aws:dynamodb:{}:{}:table/{}",
3681            state.region, state.account_id, table_name
3682        );
3683
3684        let stream_arn = if stream_enabled {
3685            Some(format!(
3686                "{}/stream/{}",
3687                arn,
3688                Utc::now().format("%Y-%m-%dT%H:%M:%S.%3f")
3689            ))
3690        } else {
3691            None
3692        };
3693        let stream_arn_attr = stream_arn.clone();
3694
3695        let table = DynamoTable {
3696            name: table_name.to_string(),
3697            arn: arn.clone(),
3698            table_id: Uuid::new_v4().to_string().replace('-', ""),
3699            key_schema,
3700            attribute_definitions,
3701            provisioned_throughput,
3702            items: Vec::new(),
3703            gsi: Vec::new(),
3704            lsi: Vec::new(),
3705            tags: BTreeMap::new(),
3706            created_at: Utc::now(),
3707            status: "ACTIVE".to_string(),
3708            item_count: 0,
3709            size_bytes: 0,
3710            billing_mode,
3711            ttl_attribute: None,
3712            ttl_enabled: false,
3713            resource_policy: None,
3714            pitr_enabled: false,
3715            kinesis_destinations: Vec::new(),
3716            contributor_insights_status: "DISABLED".to_string(),
3717            contributor_insights_counters: BTreeMap::new(),
3718            stream_enabled,
3719            stream_view_type,
3720            stream_arn,
3721            stream_records: Arc::new(RwLock::new(Vec::new())),
3722            sse_type: None,
3723            sse_kms_key_arn: None,
3724            deletion_protection_enabled,
3725            on_demand_throughput,
3726        };
3727
3728        state.tables.insert(table_name.to_string(), table);
3729        let mut result = ProvisionResult::new(arn.clone()).with("Arn", arn);
3730        if let Some(stream_arn_value) = stream_arn_attr {
3731            result = result.with("StreamArn", stream_arn_value);
3732        }
3733        Ok(result)
3734    }
3735
3736    fn delete_dynamodb_table(&self, physical_id: &str) -> Result<(), String> {
3737        let mut __ddb_mas = self.dynamodb_state.write();
3738        let state = __ddb_mas.get_or_create(&self.account_id);
3739        // physical_id is the ARN; find the table name
3740        let table_name = state
3741            .tables
3742            .iter()
3743            .find(|(_, t)| t.arn == physical_id)
3744            .map(|(name, _)| name.clone());
3745        if let Some(name) = table_name {
3746            state.tables.remove(&name);
3747        }
3748        Ok(())
3749    }
3750
3751    // --- CloudWatch Logs ---
3752
3753    fn create_log_group(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
3754        let props = &resource.properties;
3755        let log_group_name = props
3756            .get("LogGroupName")
3757            .and_then(|v| v.as_str())
3758            .unwrap_or(&resource.logical_id);
3759
3760        let retention_in_days = props
3761            .get("RetentionInDays")
3762            .and_then(|v| v.as_i64())
3763            .map(|v| v as i32);
3764
3765        let mut logs_accounts = self.logs_state.write();
3766        let state = logs_accounts.get_or_create(&self.account_id);
3767        let arn = format!(
3768            "arn:aws:logs:{}:{}:log-group:{}:*",
3769            state.region, state.account_id, log_group_name
3770        );
3771
3772        let log_group = fakecloud_logs::LogGroup {
3773            name: log_group_name.to_string(),
3774            arn: arn.clone(),
3775            creation_time: Utc::now().timestamp_millis(),
3776            retention_in_days,
3777            kms_key_id: None,
3778            stored_bytes: 0,
3779            log_streams: std::collections::BTreeMap::new(),
3780            tags: std::collections::BTreeMap::new(),
3781            subscription_filters: Vec::new(),
3782            data_protection_policy: None,
3783            index_policies: Vec::new(),
3784            transformer: None,
3785            deletion_protection: false,
3786            log_group_class: Some("STANDARD".to_string()),
3787        };
3788
3789        state
3790            .log_groups
3791            .insert(log_group_name.to_string(), log_group);
3792        Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
3793    }
3794
3795    fn create_lambda_function(
3796        &self,
3797        resource: &ResourceDefinition,
3798    ) -> Result<ProvisionResult, String> {
3799        let props = &resource.properties;
3800        let function_name = props
3801            .get("FunctionName")
3802            .and_then(|v| v.as_str())
3803            .map(|s| s.to_string())
3804            .unwrap_or_else(|| {
3805                format!(
3806                    "{}-{}-{}",
3807                    self.stack_id
3808                        .rsplit('/')
3809                        .nth(1)
3810                        .unwrap_or(&resource.logical_id),
3811                    resource.logical_id,
3812                    Uuid::new_v4()
3813                        .to_string()
3814                        .split('-')
3815                        .next()
3816                        .unwrap_or("rand")
3817                )
3818            });
3819
3820        let cfg = parse_lambda_function_props(props)?;
3821        let function_arn = format!(
3822            "arn:aws:lambda:{}:{}:function:{}",
3823            self.region, self.account_id, function_name
3824        );
3825
3826        // Resolve `Code.S3Bucket` + `Code.S3Key` against the in-process S3 state
3827        // so a stack that uploads code via `AWS::S3::Bucket` + `AWS::S3::Object`
3828        // (or any S3 PutObject before CreateStack) hydrates `code_zip` from the
3829        // real bytes. ZipFile (already decoded by parse_lambda_function_props)
3830        // wins over S3 if both are present, mirroring AWS validation order.
3831        let code_zip = if cfg.code_zip.is_some() {
3832            cfg.code_zip.clone()
3833        } else if let (Some(bucket_name), Some(key)) = (&cfg.s3_bucket, &cfg.s3_key) {
3834            Some(self.read_s3_object_bytes(bucket_name, key).map_err(|e| {
3835                format!("Failed to read Code.S3Bucket={bucket_name} Code.S3Key={key}: {e}")
3836            })?)
3837        } else {
3838            None
3839        };
3840
3841        // Hash the actual ZIP bytes when available; image-based functions
3842        // leave the sha empty (matching the lambda CreateFunction code path
3843        // when neither ZipFile nor S3 source is supplied).
3844        let (code_sha256, code_size) = match &code_zip {
3845            Some(bytes) => (sha256_b64(bytes), bytes.len() as i64),
3846            None => (String::new(), 0),
3847        };
3848
3849        // Resolve attached layer ARNs to their current code_size so
3850        // GetFunctionConfiguration echoes the right size without a
3851        // second state lookup.
3852        let layers: Vec<AttachedLayer> = {
3853            let accounts = self.lambda_state.read();
3854            cfg.layers
3855                .iter()
3856                .map(|arn| AttachedLayer {
3857                    arn: arn.clone(),
3858                    code_size: layer_code_size(&accounts, arn),
3859                })
3860                .collect()
3861        };
3862
3863        let func = fakecloud_lambda::LambdaFunction {
3864            function_name: function_name.clone(),
3865            function_arn: function_arn.clone(),
3866            runtime: cfg.runtime,
3867            role: cfg.role,
3868            handler: cfg.handler,
3869            description: cfg.description,
3870            timeout: cfg.timeout,
3871            memory_size: cfg.memory_size,
3872            code_sha256,
3873            code_size,
3874            version: "$LATEST".to_string(),
3875            last_modified: Utc::now(),
3876            tags: cfg.tags,
3877            environment: cfg.environment,
3878            architectures: cfg.architectures,
3879            package_type: cfg.package_type,
3880            code_zip,
3881            image_uri: cfg.image_uri,
3882            policy: None,
3883            layers,
3884            revision_id: Uuid::new_v4().to_string(),
3885            tracing_mode: cfg.tracing_mode,
3886            kms_key_arn: cfg.kms_key_arn,
3887            ephemeral_storage_size: cfg.ephemeral_storage_size,
3888            vpc_config: cfg.vpc_config,
3889            snap_start: cfg.snap_start,
3890            dead_letter_config_arn: cfg.dead_letter_config_arn,
3891            file_system_configs: cfg.file_system_configs,
3892            logging_config: cfg.logging_config,
3893            image_config: None,
3894            durable_config: None,
3895            signing_profile_version_arn: None,
3896            signing_job_arn: None,
3897            runtime_version_config: None,
3898            master_arn: None,
3899            state_reason: None,
3900            state_reason_code: None,
3901            last_update_status_reason: None,
3902            last_update_status_reason_code: None,
3903        };
3904
3905        let mut accounts = self.lambda_state.write();
3906        let state = accounts.get_or_create(&self.account_id);
3907        state.functions.insert(function_name.clone(), func);
3908
3909        // Capture every GetAtt-resolvable attribute eagerly. Output
3910        // resolution happens after `provision_stack_resources` returns
3911        // and only sees `StackResource.attributes`, so any attribute the
3912        // template's `Outputs` need must be in this map.
3913        Ok(ProvisionResult::new(function_name.clone())
3914            .with("Arn", function_arn)
3915            .with("FunctionName", function_name)
3916            .with("Version", "$LATEST"))
3917    }
3918
3919    /// Apply a CFN template-driven update to an existing Lambda function.
3920    /// Mirrors `UpdateFunctionConfiguration` + `UpdateFunctionCode`:
3921    /// rewrite mutable configuration fields from the new template, re-hash
3922    /// any new code bytes, bump `revision_id` and `last_modified`. The
3923    /// function's ARN, name, and `$LATEST` version stay put — those are
3924    /// the immutable identity of the resource as far as CloudFormation
3925    /// is concerned.
3926    fn update_lambda_function(
3927        &self,
3928        existing: &StackResource,
3929        resource: &ResourceDefinition,
3930    ) -> Result<ProvisionResult, String> {
3931        let props = &resource.properties;
3932        let function_name = existing.physical_id.clone();
3933        let cfg = parse_lambda_function_props(props)?;
3934
3935        let new_code_zip = if cfg.code_zip.is_some() {
3936            cfg.code_zip.clone()
3937        } else if let (Some(bucket_name), Some(key)) = (&cfg.s3_bucket, &cfg.s3_key) {
3938            Some(self.read_s3_object_bytes(bucket_name, key).map_err(|e| {
3939                format!("Failed to read Code.S3Bucket={bucket_name} Code.S3Key={key}: {e}")
3940            })?)
3941        } else {
3942            None
3943        };
3944
3945        let resolved_layers: Vec<AttachedLayer> = {
3946            let accounts = self.lambda_state.read();
3947            cfg.layers
3948                .iter()
3949                .map(|arn| AttachedLayer {
3950                    arn: arn.clone(),
3951                    code_size: layer_code_size(&accounts, arn),
3952                })
3953                .collect()
3954        };
3955
3956        let mut accounts = self.lambda_state.write();
3957        let state = accounts.get_or_create(&self.account_id);
3958        let func = state.functions.get_mut(&function_name).ok_or_else(|| {
3959            format!("Cannot update {function_name}: function does not exist in lambda state")
3960        })?;
3961
3962        func.runtime = cfg.runtime;
3963        func.role = cfg.role;
3964        func.handler = cfg.handler;
3965        func.description = cfg.description;
3966        func.timeout = cfg.timeout;
3967        func.memory_size = cfg.memory_size;
3968        func.environment = cfg.environment;
3969        func.architectures = cfg.architectures;
3970        func.package_type = cfg.package_type;
3971        func.tracing_mode = cfg.tracing_mode;
3972        func.kms_key_arn = cfg.kms_key_arn;
3973        func.ephemeral_storage_size = cfg.ephemeral_storage_size;
3974        func.vpc_config = cfg.vpc_config;
3975        func.snap_start = cfg.snap_start;
3976        func.dead_letter_config_arn = cfg.dead_letter_config_arn;
3977        func.file_system_configs = cfg.file_system_configs;
3978        func.logging_config = cfg.logging_config;
3979        func.layers = resolved_layers;
3980        if cfg.image_uri.is_some() {
3981            func.image_uri = cfg.image_uri;
3982        }
3983        if !cfg.tags.is_empty() {
3984            func.tags = cfg.tags;
3985        }
3986        if let Some(bytes) = new_code_zip {
3987            func.code_sha256 = sha256_b64(&bytes);
3988            func.code_size = bytes.len() as i64;
3989            func.code_zip = Some(bytes);
3990        }
3991        func.last_modified = Utc::now();
3992        func.revision_id = Uuid::new_v4().to_string();
3993
3994        let function_arn = func.function_arn.clone();
3995        Ok(ProvisionResult::new(function_name.clone())
3996            .with("Arn", function_arn)
3997            .with("FunctionName", function_name)
3998            .with("Version", "$LATEST"))
3999    }
4000
4001    fn delete_lambda_function(&self, physical_id: &str) -> Result<(), String> {
4002        let mut accounts = self.lambda_state.write();
4003        let state = accounts.default_mut();
4004        state.functions.remove(physical_id);
4005        Ok(())
4006    }
4007
4008    /// Look up an S3 object's bytes from the in-process S3 state. Used by
4009    /// the Lambda function provisioner to hydrate `Code.S3Bucket` /
4010    /// `Code.S3Key` references into real ZIP content. Returns an error
4011    /// string when the bucket or key is missing so the CFN error
4012    /// surfaces back to the caller.
4013    fn read_s3_object_bytes(&self, bucket: &str, key: &str) -> Result<Vec<u8>, String> {
4014        let mut accounts = self.s3_state.write();
4015        let state = accounts.get_or_create(&self.account_id);
4016        let body_ref = {
4017            let b = state
4018                .buckets
4019                .get(bucket)
4020                .ok_or_else(|| format!("S3 bucket {bucket} does not exist"))?;
4021            let object = b
4022                .objects
4023                .get(key)
4024                .ok_or_else(|| format!("S3 object s3://{bucket}/{key} does not exist"))?;
4025            object.body.clone()
4026        };
4027        // `read_body` consults the body cache (which is owned by the state),
4028        // so re-borrow `state` after dropping the bucket borrow above.
4029        state
4030            .read_body(&body_ref)
4031            .map(|b| b.to_vec())
4032            .map_err(|e| format!("S3 read failed: {e}"))
4033    }
4034
4035    fn create_lambda_permission(
4036        &self,
4037        resource: &ResourceDefinition,
4038    ) -> Result<ProvisionResult, String> {
4039        let props = &resource.properties;
4040        let function_name = parse_lambda_function_name(
4041            props
4042                .get("FunctionName")
4043                .and_then(|v| v.as_str())
4044                .ok_or_else(|| "FunctionName is required".to_string())?,
4045        );
4046        // CFN does not surface a StatementId knob; synthesize one from
4047        // the logical id so subsequent updates / deletes can find the
4048        // statement again.
4049        let statement_id = format!(
4050            "cfn-{}-{}",
4051            resource.logical_id,
4052            &Uuid::new_v4().simple().to_string()[..8]
4053        );
4054        self.append_lambda_permission_statement(&function_name, &statement_id, props)?;
4055
4056        // Encode `{function}|{sid}` so delete can target a single statement.
4057        let physical_id = format!("{function_name}|{statement_id}");
4058        Ok(ProvisionResult::new(physical_id).with("Id", statement_id))
4059    }
4060
4061    /// Build a canonical Lambda resource-policy statement from a CFN
4062    /// `AWS::Lambda::Permission` `Properties` map and append it to the
4063    /// function's stored policy. Shared by create + update so the same
4064    /// property→condition mapping applies on both paths. Returns the
4065    /// function ARN of the target so callers can echo it back if needed.
4066    fn append_lambda_permission_statement(
4067        &self,
4068        function_name: &str,
4069        statement_id: &str,
4070        props: &serde_json::Value,
4071    ) -> Result<String, String> {
4072        let action = props
4073            .get("Action")
4074            .and_then(|v| v.as_str())
4075            .ok_or_else(|| "Action is required".to_string())?
4076            .to_string();
4077        let principal = props
4078            .get("Principal")
4079            .and_then(|v| v.as_str())
4080            .ok_or_else(|| "Principal is required".to_string())?
4081            .to_string();
4082        let source_arn = props
4083            .get("SourceArn")
4084            .and_then(|v| v.as_str())
4085            .map(|s| s.to_string());
4086        let source_account = props
4087            .get("SourceAccount")
4088            .and_then(|v| v.as_str())
4089            .map(|s| s.to_string());
4090        let event_source_token = props
4091            .get("EventSourceToken")
4092            .and_then(|v| v.as_str())
4093            .map(|s| s.to_string());
4094        let function_url_auth_type = props
4095            .get("FunctionUrlAuthType")
4096            .and_then(|v| v.as_str())
4097            .map(|s| s.to_string());
4098        let principal_org_id = props
4099            .get("PrincipalOrgID")
4100            .and_then(|v| v.as_str())
4101            .map(|s| s.to_string());
4102
4103        let mut accounts = self.lambda_state.write();
4104        let state = accounts.get_or_create(&self.account_id);
4105        let func = state.functions.get_mut(function_name).ok_or_else(|| {
4106            format!(
4107                "Function {function_name} does not exist yet — retry once it has been provisioned"
4108            )
4109        })?;
4110
4111        let mut doc: serde_json::Value = func
4112            .policy
4113            .as_deref()
4114            .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
4115            .filter(|v| v.is_object())
4116            .unwrap_or_else(|| serde_json::json!({"Version": "2012-10-17", "Statement": []}));
4117        if !doc.get("Statement").map(|s| s.is_array()).unwrap_or(false) {
4118            doc["Statement"] = serde_json::json!([]);
4119        }
4120        let principal_value =
4121            if principal.ends_with(".amazonaws.com") || principal.contains(".amazon") {
4122                serde_json::json!({ "Service": principal })
4123            } else {
4124                serde_json::json!({ "AWS": principal })
4125            };
4126        let mut arn_like = serde_json::Map::new();
4127        let mut string_equals = serde_json::Map::new();
4128        if let Some(src) = source_arn {
4129            arn_like.insert("AWS:SourceArn".to_string(), serde_json::Value::String(src));
4130        }
4131        if let Some(acct) = source_account {
4132            string_equals.insert(
4133                "AWS:SourceAccount".to_string(),
4134                serde_json::Value::String(acct),
4135            );
4136        }
4137        if let Some(token) = event_source_token {
4138            string_equals.insert(
4139                "lambda:EventSourceToken".to_string(),
4140                serde_json::Value::String(token),
4141            );
4142        }
4143        if let Some(auth) = function_url_auth_type {
4144            string_equals.insert(
4145                "lambda:FunctionUrlAuthType".to_string(),
4146                serde_json::Value::String(auth),
4147            );
4148        }
4149        if let Some(org) = principal_org_id {
4150            string_equals.insert(
4151                "aws:PrincipalOrgID".to_string(),
4152                serde_json::Value::String(org),
4153            );
4154        }
4155        let mut conditions = serde_json::Map::new();
4156        if !arn_like.is_empty() {
4157            conditions.insert("ArnLike".to_string(), serde_json::Value::Object(arn_like));
4158        }
4159        if !string_equals.is_empty() {
4160            conditions.insert(
4161                "StringEquals".to_string(),
4162                serde_json::Value::Object(string_equals),
4163            );
4164        }
4165
4166        let mut statement = serde_json::Map::new();
4167        statement.insert(
4168            "Sid".to_string(),
4169            serde_json::Value::String(statement_id.to_string()),
4170        );
4171        statement.insert(
4172            "Effect".to_string(),
4173            serde_json::Value::String("Allow".to_string()),
4174        );
4175        statement.insert("Principal".to_string(), principal_value);
4176        statement.insert("Action".to_string(), serde_json::Value::String(action));
4177        statement.insert(
4178            "Resource".to_string(),
4179            serde_json::Value::String(func.function_arn.clone()),
4180        );
4181        if !conditions.is_empty() {
4182            statement.insert(
4183                "Condition".to_string(),
4184                serde_json::Value::Object(conditions),
4185            );
4186        }
4187        doc["Statement"]
4188            .as_array_mut()
4189            .unwrap()
4190            .push(serde_json::Value::Object(statement));
4191        func.policy = Some(doc.to_string());
4192        Ok(func.function_arn.clone())
4193    }
4194
4195    /// Replace the policy statement for an existing CFN-managed
4196    /// `AWS::Lambda::Permission`. CFN treats most property changes as
4197    /// in-place updates: the statement id stays put, the body is
4198    /// rewritten to reflect the new properties.
4199    fn update_lambda_permission(
4200        &self,
4201        existing: &StackResource,
4202        resource: &ResourceDefinition,
4203    ) -> Result<ProvisionResult, String> {
4204        let Some((function_name, statement_id)) = existing.physical_id.split_once('|') else {
4205            return Err(format!(
4206                "Permission physical id `{}` is malformed; expected `{{function}}|{{sid}}`",
4207                existing.physical_id
4208            ));
4209        };
4210        // First strip the prior statement so we don't end up with a duplicate
4211        // sid in the policy doc.
4212        {
4213            let mut accounts = self.lambda_state.write();
4214            let state = accounts.get_or_create(&self.account_id);
4215            if let Some(func) = state.functions.get_mut(function_name) {
4216                if let Some(policy_str) = func.policy.as_deref() {
4217                    if let Ok(mut doc) = serde_json::from_str::<serde_json::Value>(policy_str) {
4218                        if let Some(arr) = doc.get_mut("Statement").and_then(|v| v.as_array_mut()) {
4219                            arr.retain(|s| {
4220                                s.get("Sid").and_then(|v| v.as_str()) != Some(statement_id)
4221                            });
4222                            func.policy = Some(doc.to_string());
4223                        }
4224                    }
4225                }
4226            }
4227        }
4228        self.append_lambda_permission_statement(function_name, statement_id, &resource.properties)?;
4229        Ok(ProvisionResult::new(existing.physical_id.clone()).with("Id", statement_id.to_string()))
4230    }
4231
4232    fn delete_lambda_permission(&self, physical_id: &str) -> Result<(), String> {
4233        let Some((function_name, sid)) = physical_id.split_once('|') else {
4234            return Ok(());
4235        };
4236        let mut accounts = self.lambda_state.write();
4237        let state = accounts.get_or_create(&self.account_id);
4238        if let Some(func) = state.functions.get_mut(function_name) {
4239            if let Some(policy_str) = func.policy.as_deref() {
4240                if let Ok(mut doc) = serde_json::from_str::<serde_json::Value>(policy_str) {
4241                    if let Some(arr) = doc.get_mut("Statement").and_then(|v| v.as_array_mut()) {
4242                        arr.retain(|s| s.get("Sid").and_then(|v| v.as_str()) != Some(sid));
4243                        func.policy = Some(doc.to_string());
4244                    }
4245                }
4246            }
4247        }
4248        Ok(())
4249    }
4250
4251    fn create_lambda_event_source_mapping(
4252        &self,
4253        resource: &ResourceDefinition,
4254    ) -> Result<ProvisionResult, String> {
4255        let props = &resource.properties;
4256        let function_name = parse_lambda_function_name(
4257            props
4258                .get("FunctionName")
4259                .and_then(|v| v.as_str())
4260                .ok_or_else(|| "FunctionName is required".to_string())?,
4261        );
4262        let cfg = parse_lambda_event_source_mapping_props(props)?;
4263
4264        let mut accounts = self.lambda_state.write();
4265        let state = accounts.get_or_create(&self.account_id);
4266        if !state.functions.contains_key(&function_name) {
4267            return Err(format!(
4268                "Function {function_name} does not exist yet — retry once it has been provisioned"
4269            ));
4270        }
4271        let function_arn = format!(
4272            "arn:aws:lambda:{}:{}:function:{}",
4273            self.region, self.account_id, function_name
4274        );
4275        let uuid = Uuid::new_v4().to_string();
4276        let esm = EventSourceMapping {
4277            uuid: uuid.clone(),
4278            function_arn,
4279            event_source_arn: cfg.event_source_arn,
4280            batch_size: cfg.batch_size,
4281            enabled: cfg.enabled,
4282            state: if cfg.enabled {
4283                "Enabled".to_string()
4284            } else {
4285                "Disabled".to_string()
4286            },
4287            last_modified: Utc::now(),
4288            filter_patterns: cfg.filter_patterns,
4289            maximum_batching_window_in_seconds: cfg.maximum_batching_window_in_seconds,
4290            starting_position: cfg.starting_position,
4291            starting_position_timestamp: cfg.starting_position_timestamp,
4292            parallelization_factor: cfg.parallelization_factor,
4293            function_response_types: cfg.function_response_types,
4294            kms_key_arn: cfg.kms_key_arn,
4295            metrics_config: cfg.metrics_config,
4296            destination_config: cfg.destination_config,
4297            maximum_retry_attempts: cfg.maximum_retry_attempts,
4298            maximum_record_age_in_seconds: cfg.maximum_record_age_in_seconds,
4299            bisect_batch_on_function_error: cfg.bisect_batch_on_function_error,
4300            tumbling_window_in_seconds: cfg.tumbling_window_in_seconds,
4301            topics: cfg.topics,
4302            queues: cfg.queues,
4303        };
4304        state.event_source_mappings.insert(uuid.clone(), esm);
4305        Ok(ProvisionResult::new(uuid.clone()).with("Id", uuid))
4306    }
4307
4308    /// In-place update of an existing EventSourceMapping. CFN treats
4309    /// every mutable property as in-place; the EventSourceArn /
4310    /// FunctionName pair stays put (those are immutable identity).
4311    fn update_lambda_event_source_mapping(
4312        &self,
4313        existing: &StackResource,
4314        resource: &ResourceDefinition,
4315    ) -> Result<ProvisionResult, String> {
4316        let cfg = parse_lambda_event_source_mapping_props(&resource.properties)?;
4317        let mut accounts = self.lambda_state.write();
4318        let state = accounts.get_or_create(&self.account_id);
4319        let esm = state
4320            .event_source_mappings
4321            .get_mut(&existing.physical_id)
4322            .ok_or_else(|| {
4323                format!(
4324                    "EventSourceMapping {} does not exist in lambda state",
4325                    existing.physical_id
4326                )
4327            })?;
4328        esm.batch_size = cfg.batch_size;
4329        esm.enabled = cfg.enabled;
4330        esm.state = if cfg.enabled {
4331            "Enabled".to_string()
4332        } else {
4333            "Disabled".to_string()
4334        };
4335        esm.last_modified = Utc::now();
4336        esm.filter_patterns = cfg.filter_patterns;
4337        esm.maximum_batching_window_in_seconds = cfg.maximum_batching_window_in_seconds;
4338        esm.parallelization_factor = cfg.parallelization_factor;
4339        esm.function_response_types = cfg.function_response_types;
4340        esm.kms_key_arn = cfg.kms_key_arn;
4341        esm.metrics_config = cfg.metrics_config;
4342        esm.destination_config = cfg.destination_config;
4343        esm.maximum_retry_attempts = cfg.maximum_retry_attempts;
4344        esm.maximum_record_age_in_seconds = cfg.maximum_record_age_in_seconds;
4345        esm.bisect_batch_on_function_error = cfg.bisect_batch_on_function_error;
4346        esm.tumbling_window_in_seconds = cfg.tumbling_window_in_seconds;
4347        Ok(ProvisionResult::new(existing.physical_id.clone())
4348            .with("Id", existing.physical_id.clone()))
4349    }
4350
4351    fn delete_lambda_event_source_mapping(&self, physical_id: &str) -> Result<(), String> {
4352        let mut accounts = self.lambda_state.write();
4353        let state = accounts.get_or_create(&self.account_id);
4354        state.event_source_mappings.remove(physical_id);
4355        Ok(())
4356    }
4357
4358    fn create_lambda_layer_version(
4359        &self,
4360        resource: &ResourceDefinition,
4361    ) -> Result<ProvisionResult, String> {
4362        let props = &resource.properties;
4363        let layer_name = props
4364            .get("LayerName")
4365            .and_then(|v| v.as_str())
4366            .unwrap_or(&resource.logical_id)
4367            .to_string();
4368        let description = props
4369            .get("Description")
4370            .and_then(|v| v.as_str())
4371            .unwrap_or("")
4372            .to_string();
4373        let license_info = props
4374            .get("LicenseInfo")
4375            .and_then(|v| v.as_str())
4376            .unwrap_or("")
4377            .to_string();
4378        let compatible_runtimes: Vec<String> = props
4379            .get("CompatibleRuntimes")
4380            .and_then(|v| v.as_array())
4381            .map(|arr| {
4382                arr.iter()
4383                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
4384                    .collect()
4385            })
4386            .unwrap_or_default();
4387        let compatible_architectures: Vec<String> = props
4388            .get("CompatibleArchitectures")
4389            .and_then(|v| v.as_array())
4390            .map(|arr| {
4391                arr.iter()
4392                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
4393                    .collect()
4394            })
4395            .unwrap_or_default();
4396        // CFN's `Content.ZipFile` rides as base64 (per the user guide).
4397        // `Content.S3Bucket` + `Content.S3Key` hydrate the same way the
4398        // Lambda function provisioner pulls Code.S3Bucket. Either path
4399        // is optional — fakecloud accepts a layer with zero-byte content
4400        // so templates that reference an upstream-managed layer ARN
4401        // still work.
4402        let content = props.get("Content");
4403        let zip_bytes = if let Some(b64) = content
4404            .and_then(|v| v.get("ZipFile"))
4405            .and_then(|v| v.as_str())
4406        {
4407            use base64::Engine;
4408            Some(
4409                base64::engine::general_purpose::STANDARD
4410                    .decode(b64)
4411                    .map_err(|e| format!("Content.ZipFile is not valid base64: {e}"))?,
4412            )
4413        } else if let (Some(bucket), Some(key)) = (
4414            content
4415                .and_then(|c| c.get("S3Bucket"))
4416                .and_then(|v| v.as_str()),
4417            content
4418                .and_then(|c| c.get("S3Key"))
4419                .and_then(|v| v.as_str()),
4420        ) {
4421            Some(self.read_s3_object_bytes(bucket, key).map_err(|e| {
4422                format!("Failed to read Content.S3Bucket={bucket} Content.S3Key={key}: {e}")
4423            })?)
4424        } else {
4425            None
4426        };
4427
4428        let (code_sha256, code_size) = match zip_bytes.as_deref() {
4429            Some(bytes) => (sha256_b64(bytes), bytes.len() as i64),
4430            None => (String::new(), 0),
4431        };
4432
4433        let mut accounts = self.lambda_state.write();
4434        let state = accounts.get_or_create(&self.account_id);
4435        let layer_arn = format!(
4436            "arn:aws:lambda:{}:{}:layer:{}",
4437            self.region, self.account_id, layer_name
4438        );
4439        let layer = state
4440            .layers
4441            .entry(layer_name.clone())
4442            .or_insert_with(|| Layer {
4443                layer_name: layer_name.clone(),
4444                layer_arn: layer_arn.clone(),
4445                versions: Vec::new(),
4446            });
4447        let next_version = (layer.versions.len() as i64) + 1;
4448        let version_arn = format!("{}:{}", layer.layer_arn, next_version);
4449        layer.versions.push(LayerVersion {
4450            version: next_version,
4451            layer_version_arn: version_arn.clone(),
4452            description: description.clone(),
4453            created_date: Utc::now(),
4454            compatible_runtimes,
4455            license_info,
4456            policy: None,
4457            code_zip: zip_bytes,
4458            code_sha256,
4459            code_size,
4460            compatible_architectures,
4461        });
4462        Ok(ProvisionResult::new(version_arn.clone())
4463            .with("LayerVersionArn", version_arn)
4464            .with("LayerArn", layer_arn))
4465    }
4466
4467    fn delete_lambda_layer_version(&self, physical_id: &str) -> Result<(), String> {
4468        // physical_id = `{layer_arn}:{version}` — strip trailing version.
4469        let Some(idx) = physical_id.rfind(':') else {
4470            return Ok(());
4471        };
4472        let (layer_arn, version_part) = physical_id.split_at(idx);
4473        let version_part = &version_part[1..];
4474        let Ok(version) = version_part.parse::<i64>() else {
4475            return Ok(());
4476        };
4477        // ARN form: arn:aws:lambda:<region>:<account>:layer:<name>
4478        let layer_name = layer_arn.rsplit(':').next().unwrap_or("").to_string();
4479        let mut accounts = self.lambda_state.write();
4480        let state = accounts.get_or_create(&self.account_id);
4481        if let Some(layer) = state.layers.get_mut(&layer_name) {
4482            layer.versions.retain(|v| v.version != version);
4483            if layer.versions.is_empty() {
4484                state.layers.remove(&layer_name);
4485            }
4486        }
4487        Ok(())
4488    }
4489
4490    fn create_lambda_url(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
4491        let props = &resource.properties;
4492        let function_name = parse_lambda_function_name(
4493            props
4494                .get("TargetFunctionArn")
4495                .and_then(|v| v.as_str())
4496                .ok_or_else(|| "TargetFunctionArn is required".to_string())?,
4497        );
4498        // Optional qualifier — when set the URL points at an alias
4499        // (`{function_name}:{qualifier}`) rather than `$LATEST`. AWS
4500        // refuses to mint a URL for a numeric version, but the CFN
4501        // schema allows the property so we accept and round-trip it.
4502        let qualifier = props
4503            .get("Qualifier")
4504            .and_then(|v| v.as_str())
4505            .filter(|s| !s.is_empty())
4506            .map(|s| s.to_string());
4507        let auth_type = props
4508            .get("AuthType")
4509            .and_then(|v| v.as_str())
4510            .unwrap_or("NONE")
4511            .to_string();
4512        let invoke_mode = props
4513            .get("InvokeMode")
4514            .and_then(|v| v.as_str())
4515            .unwrap_or("BUFFERED")
4516            .to_string();
4517        let cors = props.get("Cors").cloned();
4518
4519        let mut accounts = self.lambda_state.write();
4520        let state = accounts.get_or_create(&self.account_id);
4521        if !state.functions.contains_key(&function_name) {
4522            return Err(format!(
4523                "Function {function_name} does not exist yet — retry once it has been provisioned"
4524            ));
4525        }
4526        let function_arn = match &qualifier {
4527            Some(q) => format!(
4528                "arn:aws:lambda:{}:{}:function:{}:{}",
4529                self.region, self.account_id, function_name, q
4530            ),
4531            None => format!(
4532                "arn:aws:lambda:{}:{}:function:{}",
4533                self.region, self.account_id, function_name
4534            ),
4535        };
4536        let function_url = format!("https://{function_name}.lambda-url.{}.on.aws/", self.region);
4537        let now = Utc::now();
4538        let cfg = FunctionUrlConfig {
4539            function_arn: function_arn.clone(),
4540            function_url: function_url.clone(),
4541            auth_type,
4542            cors,
4543            creation_time: now,
4544            last_modified_time: now,
4545            invoke_mode,
4546        };
4547        // Key by `{function}:{qualifier}` when qualifier is set so
4548        // delete and update can round-trip the same shape, while leaving
4549        // the bare-function-name URL alone for $LATEST.
4550        let key = match &qualifier {
4551            Some(q) => format!("{function_name}:{q}"),
4552            None => function_name.clone(),
4553        };
4554        state.function_url_configs.insert(key.clone(), cfg);
4555
4556        Ok(ProvisionResult::new(key)
4557            .with("FunctionArn", function_arn)
4558            .with("FunctionUrl", function_url))
4559    }
4560
4561    /// Update an existing Lambda Url config in place. AuthType, Cors,
4562    /// and InvokeMode are mutable; the target function and qualifier
4563    /// (which collectively form the key) are not — CFN replaces the
4564    /// resource if those change, so we only touch the body here.
4565    fn update_lambda_url(
4566        &self,
4567        existing: &StackResource,
4568        resource: &ResourceDefinition,
4569    ) -> Result<ProvisionResult, String> {
4570        let props = &resource.properties;
4571        let auth_type = props
4572            .get("AuthType")
4573            .and_then(|v| v.as_str())
4574            .unwrap_or("NONE")
4575            .to_string();
4576        let invoke_mode = props
4577            .get("InvokeMode")
4578            .and_then(|v| v.as_str())
4579            .unwrap_or("BUFFERED")
4580            .to_string();
4581        let cors = props.get("Cors").cloned();
4582
4583        let mut accounts = self.lambda_state.write();
4584        let state = accounts.get_or_create(&self.account_id);
4585        let cfg = state
4586            .function_url_configs
4587            .get_mut(&existing.physical_id)
4588            .ok_or_else(|| {
4589                format!(
4590                    "FunctionUrlConfig {} does not exist in lambda state",
4591                    existing.physical_id
4592                )
4593            })?;
4594        cfg.auth_type = auth_type;
4595        cfg.invoke_mode = invoke_mode;
4596        cfg.cors = cors;
4597        cfg.last_modified_time = Utc::now();
4598        let function_arn = cfg.function_arn.clone();
4599        let function_url = cfg.function_url.clone();
4600        Ok(ProvisionResult::new(existing.physical_id.clone())
4601            .with("FunctionArn", function_arn)
4602            .with("FunctionUrl", function_url))
4603    }
4604
4605    fn delete_lambda_url(&self, physical_id: &str) -> Result<(), String> {
4606        let mut accounts = self.lambda_state.write();
4607        let state = accounts.get_or_create(&self.account_id);
4608        state.function_url_configs.remove(physical_id);
4609        Ok(())
4610    }
4611
4612    fn create_lambda_alias(
4613        &self,
4614        resource: &ResourceDefinition,
4615    ) -> Result<ProvisionResult, String> {
4616        let props = &resource.properties;
4617        let function_name = parse_lambda_function_name(
4618            props
4619                .get("FunctionName")
4620                .and_then(|v| v.as_str())
4621                .ok_or_else(|| "FunctionName is required".to_string())?,
4622        );
4623        let alias_name = props
4624            .get("Name")
4625            .and_then(|v| v.as_str())
4626            .ok_or_else(|| "Name is required".to_string())?
4627            .to_string();
4628        let function_version = props
4629            .get("FunctionVersion")
4630            .and_then(|v| v.as_str())
4631            .unwrap_or("$LATEST")
4632            .to_string();
4633        let description = props
4634            .get("Description")
4635            .and_then(|v| v.as_str())
4636            .unwrap_or("")
4637            .to_string();
4638        let routing_config = props.get("RoutingConfig").cloned();
4639
4640        let mut accounts = self.lambda_state.write();
4641        let state = accounts.get_or_create(&self.account_id);
4642        if !state.functions.contains_key(&function_name) {
4643            return Err(format!(
4644                "Function {function_name} does not exist yet — retry once it has been provisioned"
4645            ));
4646        }
4647        let alias_arn = format!(
4648            "arn:aws:lambda:{}:{}:function:{}:{}",
4649            self.region, self.account_id, function_name, alias_name
4650        );
4651        let key = format!("{function_name}:{alias_name}");
4652        state.aliases.insert(
4653            key.clone(),
4654            FunctionAlias {
4655                alias_arn: alias_arn.clone(),
4656                name: alias_name,
4657                function_version,
4658                description,
4659                revision_id: Uuid::new_v4().to_string(),
4660                routing_config,
4661            },
4662        );
4663        // ProvisionedConcurrencyConfig — CFN allows attaching a target
4664        // concurrency at the same time as the alias is created. Mirror
4665        // the runtime PutProvisionedConcurrencyConfig path: stamp a
4666        // `ProvisionedConcurrencyConfig` entry keyed by `{function}:{alias}`
4667        // with a `READY` status and the requested count fully allocated.
4668        if let Some(cnt) = props
4669            .get("ProvisionedConcurrencyConfig")
4670            .and_then(|v| v.get("ProvisionedConcurrentExecutions"))
4671            .and_then(|v| v.as_i64())
4672        {
4673            state.provisioned_concurrency.insert(
4674                key.clone(),
4675                fakecloud_lambda::ProvisionedConcurrencyConfig {
4676                    requested: cnt,
4677                    allocated: cnt,
4678                    status: "READY".to_string(),
4679                    last_modified: Utc::now(),
4680                },
4681            );
4682        }
4683        Ok(ProvisionResult::new(key).with("AliasArn", alias_arn))
4684    }
4685
4686    /// Update an existing Lambda Alias in place. FunctionVersion,
4687    /// Description, RoutingConfig, and ProvisionedConcurrencyConfig are
4688    /// the mutable fields — Name and FunctionName are immutable, so a
4689    /// change there is a replacement (the parent CFN engine handles
4690    /// that path; this fn only sees the in-place case).
4691    fn update_lambda_alias(
4692        &self,
4693        existing: &StackResource,
4694        resource: &ResourceDefinition,
4695    ) -> Result<ProvisionResult, String> {
4696        let props = &resource.properties;
4697        let function_version = props
4698            .get("FunctionVersion")
4699            .and_then(|v| v.as_str())
4700            .unwrap_or("$LATEST")
4701            .to_string();
4702        let description = props
4703            .get("Description")
4704            .and_then(|v| v.as_str())
4705            .unwrap_or("")
4706            .to_string();
4707        let routing_config = props.get("RoutingConfig").cloned();
4708
4709        let mut accounts = self.lambda_state.write();
4710        let state = accounts.get_or_create(&self.account_id);
4711        let alias = state
4712            .aliases
4713            .get_mut(&existing.physical_id)
4714            .ok_or_else(|| {
4715                format!(
4716                    "Alias {} does not exist in lambda state",
4717                    existing.physical_id
4718                )
4719            })?;
4720        alias.function_version = function_version;
4721        alias.description = description;
4722        alias.routing_config = routing_config;
4723        alias.revision_id = Uuid::new_v4().to_string();
4724        let alias_arn = alias.alias_arn.clone();
4725
4726        // Re-stamp provisioned concurrency to match the new shape; remove
4727        // when the property is dropped from the template.
4728        match props
4729            .get("ProvisionedConcurrencyConfig")
4730            .and_then(|v| v.get("ProvisionedConcurrentExecutions"))
4731            .and_then(|v| v.as_i64())
4732        {
4733            Some(cnt) => {
4734                state.provisioned_concurrency.insert(
4735                    existing.physical_id.clone(),
4736                    fakecloud_lambda::ProvisionedConcurrencyConfig {
4737                        requested: cnt,
4738                        allocated: cnt,
4739                        status: "READY".to_string(),
4740                        last_modified: Utc::now(),
4741                    },
4742                );
4743            }
4744            None => {
4745                state.provisioned_concurrency.remove(&existing.physical_id);
4746            }
4747        }
4748        Ok(ProvisionResult::new(existing.physical_id.clone()).with("AliasArn", alias_arn))
4749    }
4750
4751    fn delete_lambda_alias(&self, physical_id: &str) -> Result<(), String> {
4752        let mut accounts = self.lambda_state.write();
4753        let state = accounts.get_or_create(&self.account_id);
4754        state.aliases.remove(physical_id);
4755        Ok(())
4756    }
4757
4758    fn create_lambda_version(
4759        &self,
4760        resource: &ResourceDefinition,
4761    ) -> Result<ProvisionResult, String> {
4762        let props = &resource.properties;
4763        let function_name = parse_lambda_function_name(
4764            props
4765                .get("FunctionName")
4766                .and_then(|v| v.as_str())
4767                .ok_or_else(|| "FunctionName is required".to_string())?,
4768        );
4769        // CFN's `Description` overrides the parent function's
4770        // description on this immutable snapshot; AWS does the same.
4771        // `CodeSha256`, when set, gates publish — AWS rejects publish if
4772        // the current `$LATEST` sha doesn't match the supplied value.
4773        let description_override = props
4774            .get("Description")
4775            .and_then(|v| v.as_str())
4776            .map(|s| s.to_string());
4777        let expected_sha = props
4778            .get("CodeSha256")
4779            .and_then(|v| v.as_str())
4780            .map(|s| s.to_string());
4781
4782        let mut accounts = self.lambda_state.write();
4783        let state = accounts.get_or_create(&self.account_id);
4784        let func = state
4785            .functions
4786            .get(&function_name)
4787            .ok_or_else(|| format!("Function {function_name} does not exist yet — retry once it has been provisioned"))?
4788            .clone();
4789        if let Some(expected) = &expected_sha {
4790            if !expected.is_empty() && expected != &func.code_sha256 {
4791                return Err(format!(
4792                    "PreconditionFailed: CodeSha256 mismatch on {function_name} — expected {expected}, $LATEST is {actual}",
4793                    actual = func.code_sha256,
4794                ));
4795            }
4796        }
4797        let versions = state
4798            .function_versions
4799            .entry(function_name.clone())
4800            .or_default();
4801        let next_version = (versions.len() as i64 + 1).to_string();
4802        versions.push(next_version.clone());
4803        // Snapshot current function config under this version with the
4804        // CFN-supplied description override (if any).
4805        let mut snapshot = func.clone();
4806        snapshot.version = next_version.clone();
4807        if let Some(desc) = description_override {
4808            snapshot.description = desc;
4809        }
4810        state
4811            .function_version_snapshots
4812            .entry(function_name.clone())
4813            .or_default()
4814            .insert(next_version.clone(), snapshot);
4815        let version_arn = format!(
4816            "arn:aws:lambda:{}:{}:function:{}:{}",
4817            self.region, self.account_id, function_name, next_version
4818        );
4819        let physical_id = format!("{function_name}:{next_version}");
4820        Ok(ProvisionResult::new(physical_id)
4821            .with("Version", next_version)
4822            .with("FunctionArn", version_arn))
4823    }
4824
4825    /// Update an existing Lambda Version. CFN treats numbered versions
4826    /// as immutable — any property change forces replacement (a new
4827    /// version number) at the engine level, so this fn just no-ops and
4828    /// echoes the existing physical id back. Keeping it wired keeps the
4829    /// update dispatch table consistent with the other 5 aux types.
4830    fn update_lambda_version(
4831        &self,
4832        existing: &StackResource,
4833        _resource: &ResourceDefinition,
4834    ) -> Result<ProvisionResult, String> {
4835        let mut accounts = self.lambda_state.write();
4836        let state = accounts.get_or_create(&self.account_id);
4837        let Some((function_name, version)) = existing.physical_id.split_once(':') else {
4838            return Err(format!(
4839                "Version physical id `{}` is malformed; expected `{{function}}:{{version}}`",
4840                existing.physical_id
4841            ));
4842        };
4843        let exists = state
4844            .function_version_snapshots
4845            .get(function_name)
4846            .map(|m| m.contains_key(version))
4847            .unwrap_or(false);
4848        if !exists {
4849            return Err(format!(
4850                "Version {version} for function {function_name} no longer exists in lambda state"
4851            ));
4852        }
4853        let version_arn = format!(
4854            "arn:aws:lambda:{}:{}:function:{}:{}",
4855            self.region, self.account_id, function_name, version
4856        );
4857        Ok(ProvisionResult::new(existing.physical_id.clone())
4858            .with("Version", version.to_string())
4859            .with("FunctionArn", version_arn))
4860    }
4861
4862    /// Update an existing LayerVersion. CFN treats LayerVersion as
4863    /// immutable (Content / CompatibleRuntimes / etc are all fixed once
4864    /// published), so a property change forces a new version. This fn
4865    /// no-ops to keep the dispatch table symmetric with the other aux
4866    /// types.
4867    fn update_lambda_layer_version(
4868        &self,
4869        existing: &StackResource,
4870        _resource: &ResourceDefinition,
4871    ) -> Result<ProvisionResult, String> {
4872        let arn = existing.physical_id.clone();
4873        // ARN form: arn:aws:lambda:<region>:<account>:layer:<name>:<version>
4874        let layer_arn_only = arn
4875            .rsplit_once(':')
4876            .map(|(prefix, _)| prefix.to_string())
4877            .unwrap_or_else(|| arn.clone());
4878        Ok(ProvisionResult::new(existing.physical_id.clone())
4879            .with("LayerVersionArn", arn)
4880            .with("LayerArn", layer_arn_only))
4881    }
4882
4883    fn delete_lambda_version(&self, physical_id: &str) -> Result<(), String> {
4884        let Some((function_name, version)) = physical_id.split_once(':') else {
4885            return Ok(());
4886        };
4887        let mut accounts = self.lambda_state.write();
4888        let state = accounts.get_or_create(&self.account_id);
4889        if let Some(versions) = state.function_versions.get_mut(function_name) {
4890            versions.retain(|v| v != version);
4891        }
4892        if let Some(snapshots) = state.function_version_snapshots.get_mut(function_name) {
4893            snapshots.remove(version);
4894        }
4895        Ok(())
4896    }
4897
4898    // --- SecretsManager ---
4899
4900    fn create_secrets_manager_secret(
4901        &self,
4902        resource: &ResourceDefinition,
4903    ) -> Result<ProvisionResult, String> {
4904        let props = &resource.properties;
4905        let name = props
4906            .get("Name")
4907            .and_then(|v| v.as_str())
4908            .unwrap_or(&resource.logical_id)
4909            .to_string();
4910        let description = props
4911            .get("Description")
4912            .and_then(|v| v.as_str())
4913            .map(|s| s.to_string());
4914        let kms_key_id = props
4915            .get("KmsKeyId")
4916            .and_then(|v| v.as_str())
4917            .map(|s| s.to_string());
4918
4919        let mut accounts = self.secretsmanager_state.write();
4920        let state = accounts.get_or_create(&self.account_id);
4921        let arn = format!(
4922            "arn:aws:secretsmanager:{}:{}:secret:{}",
4923            state.region, state.account_id, name
4924        );
4925
4926        if state.secrets.contains_key(&arn) {
4927            return Err(format!("Secret {name} already exists"));
4928        }
4929
4930        let now = Utc::now();
4931        let mut versions = BTreeMap::new();
4932        let mut current_version_id: Option<String> = None;
4933        let initial_string: Option<String> =
4934            if let Some(secret_string) = props.get("SecretString").and_then(|v| v.as_str()) {
4935                Some(secret_string.to_string())
4936            } else if let Some(gen) = props.get("GenerateSecretString") {
4937                Some(generate_secret_string_payload(gen)?)
4938            } else {
4939                None
4940            };
4941        if let Some(secret_string) = initial_string {
4942            let version_id = Uuid::new_v4().to_string();
4943            versions.insert(
4944                version_id.clone(),
4945                SecretVersion {
4946                    version_id: version_id.clone(),
4947                    secret_string: Some(secret_string),
4948                    secret_binary: None,
4949                    stages: vec!["AWSCURRENT".to_string()],
4950                    created_at: now,
4951                },
4952            );
4953            current_version_id = Some(version_id);
4954        }
4955
4956        let mut tags: Vec<(String, String)> = Vec::new();
4957        if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
4958            for t in arr {
4959                if let (Some(k), Some(v)) = (
4960                    t.get("Key").and_then(|x| x.as_str()),
4961                    t.get("Value").and_then(|x| x.as_str()),
4962                ) {
4963                    tags.push((k.to_string(), v.to_string()));
4964                }
4965            }
4966        }
4967        let tags_set = !tags.is_empty();
4968
4969        let secret = Secret {
4970            name: name.clone(),
4971            arn: arn.clone(),
4972            description,
4973            kms_key_id,
4974            versions,
4975            current_version_id,
4976            tags,
4977            tags_ever_set: tags_set,
4978            deleted: false,
4979            deletion_date: None,
4980            created_at: now,
4981            last_changed_at: now,
4982            last_accessed_at: None,
4983            rotation_enabled: None,
4984            rotation_lambda_arn: None,
4985            rotation_rules: None,
4986            last_rotated_at: None,
4987            resource_policy: None,
4988        };
4989        state.secrets.insert(arn.clone(), secret);
4990
4991        Ok(ProvisionResult::new(arn.clone())
4992            .with("Id", arn.clone())
4993            .with("Name", name))
4994    }
4995
4996    fn delete_secrets_manager_secret(&self, physical_id: &str) -> Result<(), String> {
4997        let mut accounts = self.secretsmanager_state.write();
4998        let state = accounts.get_or_create(&self.account_id);
4999        state.secrets.remove(physical_id);
5000        Ok(())
5001    }
5002
5003    // --- Kinesis ---
5004
5005    fn create_kinesis_stream(
5006        &self,
5007        resource: &ResourceDefinition,
5008    ) -> Result<ProvisionResult, String> {
5009        let props = &resource.properties;
5010        let stream_name = props
5011            .get("Name")
5012            .and_then(|v| v.as_str())
5013            .unwrap_or(&resource.logical_id)
5014            .to_string();
5015        let shard_count = props
5016            .get("ShardCount")
5017            .and_then(|v| v.as_i64())
5018            .unwrap_or(1) as i32;
5019        if shard_count <= 0 {
5020            return Err("ShardCount must be greater than zero".to_string());
5021        }
5022        let stream_mode = props
5023            .get("StreamModeDetails")
5024            .and_then(|v| v.get("StreamMode"))
5025            .and_then(|v| v.as_str())
5026            .unwrap_or("PROVISIONED")
5027            .to_string();
5028        let retention_period_hours = props
5029            .get("RetentionPeriodHours")
5030            .and_then(|v| v.as_i64())
5031            .unwrap_or(24) as i32;
5032
5033        let mut accounts = self.kinesis_state.write();
5034        let state = accounts.get_or_create(&self.account_id);
5035        if state.streams.contains_key(&stream_name) {
5036            return Err(format!("Stream {stream_name} already exists"));
5037        }
5038        let stream_arn = format!(
5039            "arn:aws:kinesis:{}:{}:stream/{}",
5040            state.region, state.account_id, stream_name
5041        );
5042        let stream = KinesisStream {
5043            stream_name: stream_name.clone(),
5044            stream_arn: stream_arn.clone(),
5045            stream_status: "ACTIVE".to_string(),
5046            stream_creation_timestamp: Utc::now(),
5047            retention_period_hours,
5048            stream_mode,
5049            encryption_type: "NONE".to_string(),
5050            key_id: None,
5051            shard_count,
5052            open_shard_count: shard_count,
5053            tags: BTreeMap::new(),
5054            shards: build_stream_shards(shard_count),
5055            next_shard_index: shard_count,
5056            enhanced_metrics: Vec::new(),
5057            warm_throughput_mibps: None,
5058            max_record_size_kib: None,
5059        };
5060        state.streams.insert(stream_name.clone(), stream);
5061
5062        Ok(ProvisionResult::new(stream_name).with("Arn", stream_arn))
5063    }
5064
5065    fn delete_kinesis_stream(&self, physical_id: &str) -> Result<(), String> {
5066        let mut accounts = self.kinesis_state.write();
5067        let state = accounts.get_or_create(&self.account_id);
5068        state.streams.remove(physical_id);
5069        Ok(())
5070    }
5071
5072    fn create_kinesis_stream_consumer(
5073        &self,
5074        resource: &ResourceDefinition,
5075    ) -> Result<ProvisionResult, String> {
5076        let props = &resource.properties;
5077        let stream_arn = props
5078            .get("StreamARN")
5079            .and_then(|v| v.as_str())
5080            .ok_or_else(|| "StreamARN is required".to_string())?
5081            .to_string();
5082        let consumer_name = props
5083            .get("ConsumerName")
5084            .and_then(|v| v.as_str())
5085            .ok_or_else(|| "ConsumerName is required".to_string())?
5086            .to_string();
5087
5088        let mut accounts = self.kinesis_state.write();
5089        let state = accounts.get_or_create(&self.account_id);
5090        if state
5091            .consumers
5092            .values()
5093            .any(|c| c.stream_arn == stream_arn && c.consumer_name == consumer_name)
5094        {
5095            return Err(format!(
5096                "Consumer {consumer_name} already exists on stream {stream_arn}"
5097            ));
5098        }
5099        let now = Utc::now();
5100        let consumer_arn = format!(
5101            "{}/consumer/{}:{}",
5102            stream_arn,
5103            consumer_name,
5104            now.timestamp()
5105        );
5106        let consumer = KinesisConsumer {
5107            consumer_name: consumer_name.clone(),
5108            consumer_arn: consumer_arn.clone(),
5109            consumer_status: "ACTIVE".to_string(),
5110            consumer_creation_timestamp: now,
5111            stream_arn: stream_arn.clone(),
5112        };
5113        state.consumers.insert(consumer_arn.clone(), consumer);
5114
5115        Ok(ProvisionResult::new(consumer_arn.clone())
5116            .with("ConsumerARN", consumer_arn)
5117            .with("ConsumerName", consumer_name)
5118            .with("ConsumerStatus", "ACTIVE")
5119            .with("ConsumerCreationTimestamp", now.timestamp().to_string())
5120            .with("StreamARN", stream_arn))
5121    }
5122
5123    fn delete_kinesis_stream_consumer(&self, physical_id: &str) -> Result<(), String> {
5124        let mut accounts = self.kinesis_state.write();
5125        let state = accounts.get_or_create(&self.account_id);
5126        state.consumers.remove(physical_id);
5127        Ok(())
5128    }
5129
5130    // --- KMS ---
5131
5132    fn create_kms_key(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
5133        let input = parse_kms_key_input(&resource.properties);
5134        let (key_id, arn) =
5135            kms_provisioner::provision_key(&self.kms_state, &self.account_id, &input)?;
5136        Ok(ProvisionResult::new(key_id.clone())
5137            .with("Arn", arn)
5138            .with("KeyId", key_id))
5139    }
5140
5141    /// Update an `AWS::KMS::Key` in place. Properties that AWS treats
5142    /// as immutable (`KeySpec`, `KeyUsage`, `Origin`, `MultiRegion`)
5143    /// trigger replacement: when the diff carries one of those, this
5144    /// returns an error and the upstream stack engine recreates the
5145    /// resource.
5146    fn update_kms_key(
5147        &self,
5148        existing: &StackResource,
5149        new_def: &ResourceDefinition,
5150    ) -> Result<ProvisionResult, String> {
5151        let new_input = parse_kms_key_input(&new_def.properties);
5152        let arn = {
5153            let mut accounts = self.kms_state.write();
5154            let state = accounts.get_or_create(&self.account_id);
5155            let key = state
5156                .keys
5157                .get(&existing.physical_id)
5158                .ok_or_else(|| format!("Key '{}' does not exist", existing.physical_id))?;
5159            if key.key_spec != new_input.key_spec
5160                || key.key_usage != new_input.key_usage
5161                || key.origin != new_input.origin
5162                || key.multi_region != new_input.multi_region
5163            {
5164                return Err(
5165                    "AWS::KMS::Key updates that change KeySpec, KeyUsage, Origin, or MultiRegion require replacement"
5166                        .to_string(),
5167                );
5168            }
5169            key.arn.clone()
5170        };
5171        kms_provisioner::update_key_properties(
5172            &self.kms_state,
5173            &self.account_id,
5174            &existing.physical_id,
5175            kms_provisioner::KeyUpdate {
5176                description: Some(new_input.description.clone()),
5177                enabled: Some(new_input.enabled),
5178                key_rotation_enabled: Some(new_input.key_rotation_enabled),
5179                policy: new_input.policy.clone(),
5180                tags: Some(new_input.tags.clone()),
5181            },
5182        )?;
5183        Ok(ProvisionResult::new(existing.physical_id.clone())
5184            .with("Arn", arn)
5185            .with("KeyId", existing.physical_id.clone()))
5186    }
5187
5188    fn delete_kms_key(&self, physical_id: &str) -> Result<(), String> {
5189        let mut accounts = self.kms_state.write();
5190        let state = accounts.get_or_create(&self.account_id);
5191        state.keys.remove(physical_id);
5192        state.aliases.retain(|_, a| a.target_key_id != physical_id);
5193        Ok(())
5194    }
5195
5196    /// Provision an `AWS::KMS::ReplicaKey`. Looks up the primary key by
5197    /// arn and inserts a region-keyed replica into the same account
5198    /// state, mirroring the ReplicateKey API contract.
5199    fn create_kms_replica_key(
5200        &self,
5201        resource: &ResourceDefinition,
5202    ) -> Result<ProvisionResult, String> {
5203        let props = &resource.properties;
5204        let primary_arn = props
5205            .get("PrimaryKeyArn")
5206            .and_then(|v| v.as_str())
5207            .ok_or_else(|| "PrimaryKeyArn is required".to_string())?
5208            .to_string();
5209        let description = props
5210            .get("Description")
5211            .and_then(|v| v.as_str())
5212            .map(str::to_string);
5213        let enabled = props
5214            .get("Enabled")
5215            .and_then(|v| v.as_bool())
5216            .unwrap_or(true);
5217        let policy = parse_key_policy(props);
5218        let tags = parse_tag_list(props);
5219
5220        let (replica_key_id, replica_arn) = kms_provisioner::provision_replica_key(
5221            &self.kms_state,
5222            &self.account_id,
5223            &primary_arn,
5224            description,
5225            enabled,
5226            policy,
5227            tags,
5228        )?;
5229        Ok(ProvisionResult::new(replica_key_id.clone())
5230            .with("KeyId", replica_key_id)
5231            .with("Arn", replica_arn))
5232    }
5233
5234    /// Update an `AWS::KMS::ReplicaKey` in place. `PrimaryKeyArn`
5235    /// changes are replacement-only, like the AWS contract — we'd
5236    /// have to rebuild the replica from a different source. Other
5237    /// fields (description, enabled, policy, tags) flow through
5238    /// [`update_key_properties`].
5239    fn update_kms_replica_key(
5240        &self,
5241        existing: &StackResource,
5242        new_def: &ResourceDefinition,
5243    ) -> Result<ProvisionResult, String> {
5244        let props = &new_def.properties;
5245        let new_primary = props
5246            .get("PrimaryKeyArn")
5247            .and_then(|v| v.as_str())
5248            .ok_or_else(|| "PrimaryKeyArn is required".to_string())?;
5249        // Compare primary region from existing replica's primary_region
5250        // field. Replacement triggers when the primary key changes.
5251        {
5252            let mut accounts = self.kms_state.write();
5253            let state = accounts.get_or_create(&self.account_id);
5254            let key = state
5255                .keys
5256                .get(&existing.physical_id)
5257                .ok_or_else(|| format!("ReplicaKey '{}' does not exist", existing.physical_id))?;
5258            if let Some(existing_region) = key.primary_region.as_deref() {
5259                let parts: Vec<&str> = new_primary.split(':').collect();
5260                if parts.len() < 4 || parts[3] != existing_region {
5261                    return Err(
5262                        "AWS::KMS::ReplicaKey updates that change PrimaryKeyArn require replacement"
5263                            .to_string(),
5264                    );
5265                }
5266            }
5267        }
5268        let description = props
5269            .get("Description")
5270            .and_then(|v| v.as_str())
5271            .map(str::to_string);
5272        let enabled = props
5273            .get("Enabled")
5274            .and_then(|v| v.as_bool())
5275            .unwrap_or(true);
5276        let policy = parse_key_policy(props);
5277        let tags = parse_tag_list(props);
5278        kms_provisioner::update_key_properties(
5279            &self.kms_state,
5280            &self.account_id,
5281            &existing.physical_id,
5282            kms_provisioner::KeyUpdate {
5283                description,
5284                enabled: Some(enabled),
5285                key_rotation_enabled: None,
5286                policy,
5287                tags: Some(tags),
5288            },
5289        )?;
5290        let arn = {
5291            let mut accounts = self.kms_state.write();
5292            let state = accounts.get_or_create(&self.account_id);
5293            state
5294                .keys
5295                .get(&existing.physical_id)
5296                .map(|k| k.arn.clone())
5297                .unwrap_or_default()
5298        };
5299        Ok(ProvisionResult::new(existing.physical_id.clone())
5300            .with("KeyId", existing.physical_id.clone())
5301            .with("Arn", arn))
5302    }
5303
5304    fn delete_kms_replica_key(&self, physical_id: &str) -> Result<(), String> {
5305        let mut accounts = self.kms_state.write();
5306        let state = accounts.get_or_create(&self.account_id);
5307        state.keys.remove(physical_id);
5308        Ok(())
5309    }
5310
5311    fn create_kms_alias(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
5312        let props = &resource.properties;
5313        let alias_name = props
5314            .get("AliasName")
5315            .and_then(|v| v.as_str())
5316            .ok_or_else(|| "AliasName is required".to_string())?
5317            .to_string();
5318        let target_input = props
5319            .get("TargetKeyId")
5320            .and_then(|v| v.as_str())
5321            .ok_or_else(|| "TargetKeyId is required".to_string())?
5322            .to_string();
5323        let alias = kms_provisioner::provision_alias(
5324            &self.kms_state,
5325            &self.account_id,
5326            &alias_name,
5327            &target_input,
5328        )?;
5329        Ok(ProvisionResult::new(alias))
5330    }
5331
5332    /// Update an `AWS::KMS::Alias` in place. Changing `AliasName`
5333    /// triggers replacement (the alias is the resource's identity);
5334    /// only `TargetKeyId` is updatable here.
5335    fn update_kms_alias(
5336        &self,
5337        existing: &StackResource,
5338        new_def: &ResourceDefinition,
5339    ) -> Result<ProvisionResult, String> {
5340        let props = &new_def.properties;
5341        let new_alias_name = props
5342            .get("AliasName")
5343            .and_then(|v| v.as_str())
5344            .ok_or_else(|| "AliasName is required".to_string())?;
5345        if new_alias_name != existing.physical_id {
5346            return Err(
5347                "AWS::KMS::Alias updates that change AliasName require replacement".to_string(),
5348            );
5349        }
5350        let target_input = props
5351            .get("TargetKeyId")
5352            .and_then(|v| v.as_str())
5353            .ok_or_else(|| "TargetKeyId is required".to_string())?;
5354        kms_provisioner::update_alias_target(
5355            &self.kms_state,
5356            &self.account_id,
5357            &existing.physical_id,
5358            target_input,
5359        )?;
5360        Ok(ProvisionResult::new(existing.physical_id.clone()))
5361    }
5362
5363    fn delete_kms_alias(&self, physical_id: &str) -> Result<(), String> {
5364        let mut accounts = self.kms_state.write();
5365        let state = accounts.get_or_create(&self.account_id);
5366        state.aliases.remove(physical_id);
5367        Ok(())
5368    }
5369
5370    // --- ECR ---
5371
5372    fn create_ecr_repository(
5373        &self,
5374        resource: &ResourceDefinition,
5375    ) -> Result<ProvisionResult, String> {
5376        let props = &resource.properties;
5377        let repository_name = props
5378            .get("RepositoryName")
5379            .and_then(|v| v.as_str())
5380            .unwrap_or(&resource.logical_id)
5381            .to_string();
5382        let image_tag_mutability = props
5383            .get("ImageTagMutability")
5384            .and_then(|v| v.as_str())
5385            .unwrap_or("MUTABLE")
5386            .to_string();
5387        let scan_on_push = props
5388            .get("ImageScanningConfiguration")
5389            .and_then(|v| v.get("ScanOnPush"))
5390            .and_then(|v| v.as_bool())
5391            .unwrap_or(false);
5392        let encryption_type = props
5393            .get("EncryptionConfiguration")
5394            .and_then(|v| v.get("EncryptionType"))
5395            .and_then(|v| v.as_str())
5396            .unwrap_or("AES256")
5397            .to_string();
5398        let kms_key = props
5399            .get("EncryptionConfiguration")
5400            .and_then(|v| v.get("KmsKey"))
5401            .and_then(|v| v.as_str())
5402            .map(|s| s.to_string());
5403        let policy_text = props
5404            .get("RepositoryPolicyText")
5405            .map(|v| {
5406                if v.is_string() {
5407                    v.as_str().unwrap_or("").to_string()
5408                } else {
5409                    serde_json::to_string(v).unwrap_or_default()
5410                }
5411            })
5412            .filter(|s| !s.is_empty());
5413        let lifecycle_policy = props
5414            .get("LifecyclePolicy")
5415            .and_then(|v| v.get("LifecyclePolicyText"))
5416            .and_then(|v| v.as_str())
5417            .map(|s| s.to_string());
5418        let mut tags: BTreeMap<String, String> = BTreeMap::new();
5419        if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
5420            for t in arr {
5421                if let (Some(k), Some(v)) = (
5422                    t.get("Key").and_then(|x| x.as_str()),
5423                    t.get("Value").and_then(|x| x.as_str()),
5424                ) {
5425                    tags.insert(k.to_string(), v.to_string());
5426                }
5427            }
5428        }
5429
5430        let empty_on_delete = props
5431            .get("EmptyOnDelete")
5432            .and_then(|v| v.as_bool())
5433            .unwrap_or(false);
5434
5435        let mut accounts = self.ecr_state.write();
5436        let state = accounts.get_or_create(&self.account_id);
5437        if state.repositories.contains_key(&repository_name) {
5438            return Err(format!("Repository {repository_name} already exists"));
5439        }
5440        let arn = state.repository_arn(&repository_name);
5441        let registry_id = state.account_id.clone();
5442        let endpoint = format!(
5443            "{}.dkr.ecr.{}.amazonaws.com",
5444            state.account_id, state.region
5445        );
5446        let mut repo = Repository::new(&repository_name, arn.clone(), &registry_id, &endpoint);
5447        repo.image_tag_mutability = image_tag_mutability;
5448        repo.image_scanning_configuration.scan_on_push = scan_on_push;
5449        repo.encryption_configuration.encryption_type = encryption_type;
5450        repo.encryption_configuration.kms_key = kms_key;
5451        repo.policy = policy_text;
5452        // Apply lifecycle policy synchronously so the store reflects the
5453        // initial prune, matching the PutLifecyclePolicy data path.
5454        if let Some(policy) = lifecycle_policy.as_ref() {
5455            let prune = fakecloud_ecr::evaluate_lifecycle_policy(&repo, policy);
5456            for digest in &prune {
5457                repo.images.remove(digest);
5458                repo.image_tags.retain(|_, d| d != digest);
5459            }
5460            repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5461        }
5462        repo.lifecycle_policy = lifecycle_policy;
5463        repo.tags = tags;
5464        let uri = repo.repository_uri.clone();
5465        state.repositories.insert(repository_name.clone(), repo);
5466
5467        Ok(ProvisionResult::new(repository_name)
5468            .with("Arn", arn)
5469            .with("RepositoryUri", uri)
5470            .with("RegistryId", registry_id)
5471            .with("EmptyOnDelete", empty_on_delete.to_string()))
5472    }
5473
5474    fn delete_ecr_repository(&self, physical_id: &str) -> Result<(), String> {
5475        let mut accounts = self.ecr_state.write();
5476        let state = accounts.get_or_create(&self.account_id);
5477        state.repositories.remove(physical_id);
5478        Ok(())
5479    }
5480
5481    /// Provision a standalone `AWS::ECR::RepositoryPolicy` against an
5482    /// existing repository. Physical id encodes `account/repo` so the
5483    /// delete path can find the right repository.
5484    fn create_ecr_repository_policy(
5485        &self,
5486        resource: &ResourceDefinition,
5487    ) -> Result<ProvisionResult, String> {
5488        let props = &resource.properties;
5489        let repository_name = props
5490            .get("RepositoryName")
5491            .and_then(|v| v.as_str())
5492            .ok_or_else(|| "RepositoryName is required".to_string())?
5493            .to_string();
5494        let policy_text = props
5495            .get("PolicyText")
5496            .map(|v| {
5497                if v.is_string() {
5498                    v.as_str().unwrap_or("").to_string()
5499                } else {
5500                    serde_json::to_string(v).unwrap_or_default()
5501                }
5502            })
5503            .ok_or_else(|| "PolicyText is required".to_string())?;
5504        let mut accounts = self.ecr_state.write();
5505        let state = accounts.get_or_create(&self.account_id);
5506        let repo = state
5507            .repositories
5508            .get_mut(&repository_name)
5509            .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5510        repo.policy = Some(policy_text);
5511        Ok(ProvisionResult::new(format!(
5512            "{}/{}",
5513            self.account_id, repository_name
5514        )))
5515    }
5516
5517    fn delete_ecr_repository_policy(&self, physical_id: &str) -> Result<(), String> {
5518        let repository_name = physical_id
5519            .split_once('/')
5520            .map(|(_, n)| n.to_string())
5521            .unwrap_or_else(|| physical_id.to_string());
5522        let mut accounts = self.ecr_state.write();
5523        let state = accounts.get_or_create(&self.account_id);
5524        if let Some(repo) = state.repositories.get_mut(&repository_name) {
5525            repo.policy = None;
5526        }
5527        Ok(())
5528    }
5529
5530    /// Set the registry-wide policy for the active account. Physical id
5531    /// is just the account id since the registry is a singleton.
5532    fn create_ecr_registry_policy(
5533        &self,
5534        resource: &ResourceDefinition,
5535    ) -> Result<ProvisionResult, String> {
5536        let props = &resource.properties;
5537        let policy_text = props
5538            .get("PolicyText")
5539            .map(|v| {
5540                if v.is_string() {
5541                    v.as_str().unwrap_or("").to_string()
5542                } else {
5543                    serde_json::to_string(v).unwrap_or_default()
5544                }
5545            })
5546            .ok_or_else(|| "PolicyText is required".to_string())?;
5547        let mut accounts = self.ecr_state.write();
5548        let state = accounts.get_or_create(&self.account_id);
5549        state.registry_policy = Some(policy_text);
5550        Ok(ProvisionResult::new(self.account_id.clone())
5551            .with("RegistryId", self.account_id.clone()))
5552    }
5553
5554    fn delete_ecr_registry_policy(&self) -> Result<(), String> {
5555        let mut accounts = self.ecr_state.write();
5556        let state = accounts.get_or_create(&self.account_id);
5557        state.registry_policy = None;
5558        Ok(())
5559    }
5560
5561    /// Provision the singleton `AWS::ECR::ReplicationConfiguration`.
5562    fn create_ecr_replication_configuration(
5563        &self,
5564        resource: &ResourceDefinition,
5565    ) -> Result<ProvisionResult, String> {
5566        use fakecloud_ecr::state::{
5567            ReplicationConfiguration, ReplicationDestination, ReplicationRule, RepositoryFilter,
5568        };
5569        let cfg = resource
5570            .properties
5571            .get("ReplicationConfiguration")
5572            .ok_or_else(|| "ReplicationConfiguration is required".to_string())?;
5573        let rules: Vec<ReplicationRule> = cfg
5574            .get("Rules")
5575            .and_then(|v| v.as_array())
5576            .map(|arr| {
5577                arr.iter()
5578                    .map(|r| {
5579                        let destinations: Vec<ReplicationDestination> = r
5580                            .get("Destinations")
5581                            .and_then(|v| v.as_array())
5582                            .map(|d| {
5583                                d.iter()
5584                                    .map(|dest| ReplicationDestination {
5585                                        region: dest
5586                                            .get("Region")
5587                                            .and_then(|v| v.as_str())
5588                                            .unwrap_or_default()
5589                                            .to_string(),
5590                                        registry_id: dest
5591                                            .get("RegistryId")
5592                                            .and_then(|v| v.as_str())
5593                                            .unwrap_or_default()
5594                                            .to_string(),
5595                                    })
5596                                    .collect()
5597                            })
5598                            .unwrap_or_default();
5599                        let repository_filters: Vec<RepositoryFilter> = r
5600                            .get("RepositoryFilters")
5601                            .and_then(|v| v.as_array())
5602                            .map(|f| {
5603                                f.iter()
5604                                    .map(|f| RepositoryFilter {
5605                                        filter: f
5606                                            .get("Filter")
5607                                            .and_then(|v| v.as_str())
5608                                            .unwrap_or_default()
5609                                            .to_string(),
5610                                        filter_type: f
5611                                            .get("FilterType")
5612                                            .and_then(|v| v.as_str())
5613                                            .unwrap_or_default()
5614                                            .to_string(),
5615                                    })
5616                                    .collect()
5617                            })
5618                            .unwrap_or_default();
5619                        ReplicationRule {
5620                            destinations,
5621                            repository_filters,
5622                        }
5623                    })
5624                    .collect()
5625            })
5626            .unwrap_or_default();
5627        let mut accounts = self.ecr_state.write();
5628        let state = accounts.get_or_create(&self.account_id);
5629        state.replication_configuration = Some(ReplicationConfiguration { rules });
5630        Ok(ProvisionResult::new(self.account_id.clone()))
5631    }
5632
5633    fn delete_ecr_replication_configuration(&self) -> Result<(), String> {
5634        let mut accounts = self.ecr_state.write();
5635        let state = accounts.get_or_create(&self.account_id);
5636        state.replication_configuration = None;
5637        Ok(())
5638    }
5639
5640    fn create_ecr_pull_through_cache_rule(
5641        &self,
5642        resource: &ResourceDefinition,
5643    ) -> Result<ProvisionResult, String> {
5644        use fakecloud_ecr::state::PullThroughCacheRule;
5645        let props = &resource.properties;
5646        let prefix = props
5647            .get("EcrRepositoryPrefix")
5648            .and_then(|v| v.as_str())
5649            .ok_or_else(|| "EcrRepositoryPrefix is required".to_string())?
5650            .to_string();
5651        let upstream_url = props
5652            .get("UpstreamRegistryUrl")
5653            .and_then(|v| v.as_str())
5654            .ok_or_else(|| "UpstreamRegistryUrl is required".to_string())?
5655            .to_string();
5656        let upstream_registry = props
5657            .get("UpstreamRegistry")
5658            .and_then(|v| v.as_str())
5659            .map(|s| s.to_string());
5660        let credential_arn = props
5661            .get("CredentialArn")
5662            .and_then(|v| v.as_str())
5663            .map(|s| s.to_string());
5664        let custom_role_arn = props
5665            .get("CustomRoleArn")
5666            .and_then(|v| v.as_str())
5667            .map(|s| s.to_string());
5668        let now = Utc::now();
5669        let rule = PullThroughCacheRule {
5670            ecr_repository_prefix: prefix.clone(),
5671            upstream_registry_url: upstream_url,
5672            upstream_registry,
5673            credential_arn,
5674            created_at: now,
5675            updated_at: now,
5676            custom_role_arn,
5677        };
5678        let mut accounts = self.ecr_state.write();
5679        let state = accounts.get_or_create(&self.account_id);
5680        state.pull_through_cache_rules.insert(prefix.clone(), rule);
5681        Ok(ProvisionResult::new(prefix))
5682    }
5683
5684    fn delete_ecr_pull_through_cache_rule(&self, physical_id: &str) -> Result<(), String> {
5685        let mut accounts = self.ecr_state.write();
5686        let state = accounts.get_or_create(&self.account_id);
5687        state.pull_through_cache_rules.remove(physical_id);
5688        Ok(())
5689    }
5690
5691    /// Provision a standalone `AWS::ECR::LifecyclePolicy` against an
5692    /// existing repository. Mirrors the `PutLifecyclePolicy` data path:
5693    /// the policy text is stored on the repo and applied synchronously
5694    /// so the store reflects any prune set immediately. Physical id
5695    /// encodes `account/repo` so delete can find the right repository.
5696    fn create_ecr_lifecycle_policy(
5697        &self,
5698        resource: &ResourceDefinition,
5699    ) -> Result<ProvisionResult, String> {
5700        let props = &resource.properties;
5701        let repository_name = props
5702            .get("RepositoryName")
5703            .and_then(|v| v.as_str())
5704            .ok_or_else(|| "RepositoryName is required".to_string())?
5705            .to_string();
5706        let policy_text = props
5707            .get("LifecyclePolicyText")
5708            .map(|v| {
5709                if v.is_string() {
5710                    v.as_str().unwrap_or("").to_string()
5711                } else {
5712                    serde_json::to_string(v).unwrap_or_default()
5713                }
5714            })
5715            .ok_or_else(|| "LifecyclePolicyText is required".to_string())?;
5716        // RegistryId is informational on AWS — accepted but the registry
5717        // lookup is always the active account in fakecloud.
5718        let _registry_id = props
5719            .get("RegistryId")
5720            .and_then(|v| v.as_str())
5721            .map(|s| s.to_string());
5722        let mut accounts = self.ecr_state.write();
5723        let state = accounts.get_or_create(&self.account_id);
5724        let repo = state
5725            .repositories
5726            .get_mut(&repository_name)
5727            .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5728        // Run the policy once now so callers see the same prune-on-write
5729        // behaviour they get from the JSON `PutLifecyclePolicy` endpoint.
5730        let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, &policy_text);
5731        for digest in &prune {
5732            repo.images.remove(digest);
5733            repo.image_tags.retain(|_, d| d != digest);
5734        }
5735        repo.lifecycle_policy = Some(policy_text);
5736        repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5737        let registry_id = repo.registry_id.clone();
5738        Ok(
5739            ProvisionResult::new(format!("{}/{}", self.account_id, repository_name))
5740                .with("RepositoryName", repository_name)
5741                .with("RegistryId", registry_id),
5742        )
5743    }
5744
5745    fn delete_ecr_lifecycle_policy(&self, physical_id: &str) -> Result<(), String> {
5746        let repository_name = physical_id
5747            .split_once('/')
5748            .map(|(_, n)| n.to_string())
5749            .unwrap_or_else(|| physical_id.to_string());
5750        let mut accounts = self.ecr_state.write();
5751        let state = accounts.get_or_create(&self.account_id);
5752        if let Some(repo) = state.repositories.get_mut(&repository_name) {
5753            repo.lifecycle_policy = None;
5754            repo.lifecycle_policy_last_evaluated_at = None;
5755        }
5756        Ok(())
5757    }
5758
5759    /// Provision the singleton `AWS::ECR::RegistryScanningConfiguration`.
5760    /// One per account; later applies overwrite the prior config —
5761    /// matching `PutRegistryScanningConfiguration` semantics.
5762    fn create_ecr_registry_scanning_configuration(
5763        &self,
5764        resource: &ResourceDefinition,
5765    ) -> Result<ProvisionResult, String> {
5766        use fakecloud_ecr::state::{
5767            RegistryScanningConfiguration, RegistryScanningRule, RepositoryFilter,
5768        };
5769        let props = &resource.properties;
5770        let scan_type = props
5771            .get("ScanType")
5772            .and_then(|v| v.as_str())
5773            .unwrap_or("BASIC")
5774            .to_string();
5775        let rules: Vec<RegistryScanningRule> = props
5776            .get("Rules")
5777            .and_then(|v| v.as_array())
5778            .map(|arr| {
5779                arr.iter()
5780                    .map(|r| {
5781                        let scan_frequency = r
5782                            .get("ScanFrequency")
5783                            .and_then(|v| v.as_str())
5784                            .unwrap_or("MANUAL")
5785                            .to_string();
5786                        let repository_filters: Vec<RepositoryFilter> = r
5787                            .get("RepositoryFilters")
5788                            .and_then(|v| v.as_array())
5789                            .map(|f| {
5790                                f.iter()
5791                                    .map(|f| RepositoryFilter {
5792                                        filter: f
5793                                            .get("Filter")
5794                                            .and_then(|v| v.as_str())
5795                                            .unwrap_or_default()
5796                                            .to_string(),
5797                                        filter_type: f
5798                                            .get("FilterType")
5799                                            .and_then(|v| v.as_str())
5800                                            .unwrap_or_default()
5801                                            .to_string(),
5802                                    })
5803                                    .collect()
5804                            })
5805                            .unwrap_or_default();
5806                        RegistryScanningRule {
5807                            scan_frequency,
5808                            repository_filters,
5809                        }
5810                    })
5811                    .collect()
5812            })
5813            .unwrap_or_default();
5814        let mut accounts = self.ecr_state.write();
5815        let state = accounts.get_or_create(&self.account_id);
5816        state.registry_scanning_configuration = RegistryScanningConfiguration { scan_type, rules };
5817        Ok(ProvisionResult::new(self.account_id.clone()))
5818    }
5819
5820    fn delete_ecr_registry_scanning_configuration(&self) -> Result<(), String> {
5821        use fakecloud_ecr::state::RegistryScanningConfiguration;
5822        let mut accounts = self.ecr_state.write();
5823        let state = accounts.get_or_create(&self.account_id);
5824        // CFN delete reverts to the AWS default (BASIC, no rules).
5825        state.registry_scanning_configuration = RegistryScanningConfiguration::default();
5826        Ok(())
5827    }
5828
5829    // ECR update_resource handlers. RepositoryName / EcrRepositoryPrefix
5830    // are immutable on AWS; CFN replaces the resource if they change.
5831    // These handlers refresh the mutable fields and keep the physical id
5832    // stable.
5833
5834    fn update_ecr_repository(
5835        &self,
5836        existing: &StackResource,
5837        resource: &ResourceDefinition,
5838    ) -> Result<ProvisionResult, String> {
5839        let props = &resource.properties;
5840        let repository_name = existing.physical_id.clone();
5841        let mut accounts = self.ecr_state.write();
5842        let state = accounts.get_or_create(&self.account_id);
5843        let repo = state
5844            .repositories
5845            .get_mut(&repository_name)
5846            .ok_or_else(|| format!("Repository {repository_name} no longer exists"))?;
5847        if let Some(s) = props.get("ImageTagMutability").and_then(|v| v.as_str()) {
5848            repo.image_tag_mutability = s.to_string();
5849        }
5850        if let Some(b) = props
5851            .get("ImageScanningConfiguration")
5852            .and_then(|v| v.get("ScanOnPush"))
5853            .and_then(|v| v.as_bool())
5854        {
5855            repo.image_scanning_configuration.scan_on_push = b;
5856        }
5857        if let Some(cfg) = props.get("EncryptionConfiguration") {
5858            if let Some(s) = cfg.get("EncryptionType").and_then(|v| v.as_str()) {
5859                repo.encryption_configuration.encryption_type = s.to_string();
5860            }
5861            if let Some(s) = cfg.get("KmsKey").and_then(|v| v.as_str()) {
5862                repo.encryption_configuration.kms_key = Some(s.to_string());
5863            }
5864        }
5865        if let Some(v) = props.get("RepositoryPolicyText") {
5866            let text = if v.is_string() {
5867                v.as_str().unwrap_or("").to_string()
5868            } else {
5869                serde_json::to_string(v).unwrap_or_default()
5870            };
5871            repo.policy = if text.is_empty() { None } else { Some(text) };
5872        }
5873        if let Some(text) = props
5874            .get("LifecyclePolicy")
5875            .and_then(|v| v.get("LifecyclePolicyText"))
5876            .and_then(|v| v.as_str())
5877        {
5878            let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, text);
5879            for digest in &prune {
5880                repo.images.remove(digest);
5881                repo.image_tags.retain(|_, d| d != digest);
5882            }
5883            repo.lifecycle_policy = Some(text.to_string());
5884            repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5885        }
5886        if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
5887            let mut tags: BTreeMap<String, String> = BTreeMap::new();
5888            for t in arr {
5889                if let (Some(k), Some(v)) = (
5890                    t.get("Key").and_then(|x| x.as_str()),
5891                    t.get("Value").and_then(|x| x.as_str()),
5892                ) {
5893                    tags.insert(k.to_string(), v.to_string());
5894                }
5895            }
5896            repo.tags = tags;
5897        }
5898        let arn = repo.repository_arn.clone();
5899        let uri = repo.repository_uri.clone();
5900        let registry_id = repo.registry_id.clone();
5901        Ok(ProvisionResult::new(repository_name)
5902            .with("Arn", arn)
5903            .with("RepositoryUri", uri)
5904            .with("RegistryId", registry_id))
5905    }
5906
5907    fn update_ecr_repository_policy(
5908        &self,
5909        existing: &StackResource,
5910        resource: &ResourceDefinition,
5911    ) -> Result<ProvisionResult, String> {
5912        let props = &resource.properties;
5913        let physical_id = existing.physical_id.clone();
5914        let repository_name = physical_id
5915            .split_once('/')
5916            .map(|(_, n)| n.to_string())
5917            .unwrap_or_else(|| physical_id.clone());
5918        let policy_text = props
5919            .get("PolicyText")
5920            .map(|v| {
5921                if v.is_string() {
5922                    v.as_str().unwrap_or("").to_string()
5923                } else {
5924                    serde_json::to_string(v).unwrap_or_default()
5925                }
5926            })
5927            .ok_or_else(|| "PolicyText is required".to_string())?;
5928        let mut accounts = self.ecr_state.write();
5929        let state = accounts.get_or_create(&self.account_id);
5930        let repo = state
5931            .repositories
5932            .get_mut(&repository_name)
5933            .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5934        repo.policy = Some(policy_text);
5935        Ok(ProvisionResult::new(physical_id))
5936    }
5937
5938    fn update_ecr_lifecycle_policy(
5939        &self,
5940        existing: &StackResource,
5941        resource: &ResourceDefinition,
5942    ) -> Result<ProvisionResult, String> {
5943        let props = &resource.properties;
5944        let physical_id = existing.physical_id.clone();
5945        let repository_name = physical_id
5946            .split_once('/')
5947            .map(|(_, n)| n.to_string())
5948            .unwrap_or_else(|| physical_id.clone());
5949        let policy_text = props
5950            .get("LifecyclePolicyText")
5951            .map(|v| {
5952                if v.is_string() {
5953                    v.as_str().unwrap_or("").to_string()
5954                } else {
5955                    serde_json::to_string(v).unwrap_or_default()
5956                }
5957            })
5958            .ok_or_else(|| "LifecyclePolicyText is required".to_string())?;
5959        let mut accounts = self.ecr_state.write();
5960        let state = accounts.get_or_create(&self.account_id);
5961        let repo = state
5962            .repositories
5963            .get_mut(&repository_name)
5964            .ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
5965        let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, &policy_text);
5966        for digest in &prune {
5967            repo.images.remove(digest);
5968            repo.image_tags.retain(|_, d| d != digest);
5969        }
5970        repo.lifecycle_policy = Some(policy_text);
5971        repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
5972        let registry_id = repo.registry_id.clone();
5973        Ok(ProvisionResult::new(physical_id)
5974            .with("RepositoryName", repository_name)
5975            .with("RegistryId", registry_id))
5976    }
5977
5978    fn update_ecr_registry_policy(
5979        &self,
5980        existing: &StackResource,
5981        resource: &ResourceDefinition,
5982    ) -> Result<ProvisionResult, String> {
5983        let props = &resource.properties;
5984        let policy_text = props
5985            .get("PolicyText")
5986            .map(|v| {
5987                if v.is_string() {
5988                    v.as_str().unwrap_or("").to_string()
5989                } else {
5990                    serde_json::to_string(v).unwrap_or_default()
5991                }
5992            })
5993            .ok_or_else(|| "PolicyText is required".to_string())?;
5994        let mut accounts = self.ecr_state.write();
5995        let state = accounts.get_or_create(&self.account_id);
5996        state.registry_policy = Some(policy_text);
5997        Ok(ProvisionResult::new(existing.physical_id.clone())
5998            .with("RegistryId", self.account_id.clone()))
5999    }
6000
6001    fn update_ecr_replication_configuration(
6002        &self,
6003        existing: &StackResource,
6004        resource: &ResourceDefinition,
6005    ) -> Result<ProvisionResult, String> {
6006        // Updates fully replace the prior config — same as create.
6007        let result = self.create_ecr_replication_configuration(resource)?;
6008        Ok(ProvisionResult::new(existing.physical_id.clone()).merge_attributes(result.attributes))
6009    }
6010
6011    fn update_ecr_registry_scanning_configuration(
6012        &self,
6013        existing: &StackResource,
6014        resource: &ResourceDefinition,
6015    ) -> Result<ProvisionResult, String> {
6016        let result = self.create_ecr_registry_scanning_configuration(resource)?;
6017        Ok(ProvisionResult::new(existing.physical_id.clone()).merge_attributes(result.attributes))
6018    }
6019
6020    fn update_ecr_pull_through_cache_rule(
6021        &self,
6022        existing: &StackResource,
6023        resource: &ResourceDefinition,
6024    ) -> Result<ProvisionResult, String> {
6025        let props = &resource.properties;
6026        let prefix = existing.physical_id.clone();
6027        let mut accounts = self.ecr_state.write();
6028        let state = accounts.get_or_create(&self.account_id);
6029        let rule = state
6030            .pull_through_cache_rules
6031            .get_mut(&prefix)
6032            .ok_or_else(|| format!("PullThroughCacheRule {prefix} no longer exists"))?;
6033        if let Some(s) = props.get("UpstreamRegistryUrl").and_then(|v| v.as_str()) {
6034            rule.upstream_registry_url = s.to_string();
6035        }
6036        if let Some(s) = props.get("UpstreamRegistry").and_then(|v| v.as_str()) {
6037            rule.upstream_registry = Some(s.to_string());
6038        }
6039        if let Some(s) = props.get("CredentialArn").and_then(|v| v.as_str()) {
6040            rule.credential_arn = Some(s.to_string());
6041        }
6042        if let Some(s) = props.get("CustomRoleArn").and_then(|v| v.as_str()) {
6043            rule.custom_role_arn = Some(s.to_string());
6044        }
6045        rule.updated_at = Utc::now();
6046        Ok(ProvisionResult::new(prefix))
6047    }
6048
6049    fn get_att_ecr_repository(&self, physical_id: &str, attribute: &str) -> Option<String> {
6050        let mut accounts = self.ecr_state.write();
6051        let state = accounts.get_or_create(&self.account_id);
6052        let repo = state.repositories.get(physical_id)?;
6053        match attribute {
6054            "Arn" => Some(repo.repository_arn.clone()),
6055            "RepositoryUri" => Some(repo.repository_uri.clone()),
6056            "RegistryId" => Some(repo.registry_id.clone()),
6057            _ => None,
6058        }
6059    }
6060
6061    // --- CloudWatch ---
6062
6063    fn create_cloudwatch_alarm(
6064        &self,
6065        resource: &ResourceDefinition,
6066    ) -> Result<ProvisionResult, String> {
6067        let props = &resource.properties;
6068        let alarm_name = props
6069            .get("AlarmName")
6070            .and_then(|v| v.as_str())
6071            .unwrap_or(&resource.logical_id)
6072            .to_string();
6073        let alarm_description = props
6074            .get("AlarmDescription")
6075            .and_then(|v| v.as_str())
6076            .map(|s| s.to_string());
6077        let actions_enabled = props
6078            .get("ActionsEnabled")
6079            .and_then(|v| v.as_bool())
6080            .unwrap_or(true);
6081        let str_array = |key: &str| -> Vec<String> {
6082            props
6083                .get(key)
6084                .and_then(|v| v.as_array())
6085                .map(|arr| {
6086                    arr.iter()
6087                        .filter_map(|x| x.as_str().map(|s| s.to_string()))
6088                        .collect()
6089                })
6090                .unwrap_or_default()
6091        };
6092        let alarm_actions = str_array("AlarmActions");
6093        let ok_actions = str_array("OKActions");
6094        let insufficient_data_actions = str_array("InsufficientDataActions");
6095
6096        let metric_name = props
6097            .get("MetricName")
6098            .and_then(|v| v.as_str())
6099            .map(|s| s.to_string());
6100        let namespace = props
6101            .get("Namespace")
6102            .and_then(|v| v.as_str())
6103            .map(|s| s.to_string());
6104        let statistic = props
6105            .get("Statistic")
6106            .and_then(|v| v.as_str())
6107            .map(|s| s.to_string());
6108        let extended_statistic = props
6109            .get("ExtendedStatistic")
6110            .and_then(|v| v.as_str())
6111            .map(|s| s.to_string());
6112        let unit = props
6113            .get("Unit")
6114            .and_then(|v| v.as_str())
6115            .map(|s| s.to_string());
6116        let period = props.get("Period").and_then(|v| v.as_i64());
6117        let evaluation_periods = props
6118            .get("EvaluationPeriods")
6119            .and_then(|v| v.as_i64())
6120            .unwrap_or(1);
6121        let datapoints_to_alarm = props.get("DatapointsToAlarm").and_then(|v| v.as_i64());
6122        let threshold = props.get("Threshold").and_then(|v| v.as_f64());
6123        let comparison_operator = props
6124            .get("ComparisonOperator")
6125            .and_then(|v| v.as_str())
6126            .unwrap_or("GreaterThanThreshold")
6127            .to_string();
6128        let treat_missing_data = props
6129            .get("TreatMissingData")
6130            .and_then(|v| v.as_str())
6131            .map(|s| s.to_string());
6132        let evaluate_low_sample_count_percentile = props
6133            .get("EvaluateLowSampleCountPercentile")
6134            .and_then(|v| v.as_str())
6135            .map(|s| s.to_string());
6136
6137        let mut dimensions: BTreeMap<String, String> = BTreeMap::new();
6138        if let Some(arr) = props.get("Dimensions").and_then(|v| v.as_array()) {
6139            for d in arr {
6140                if let (Some(k), Some(v)) = (
6141                    d.get("Name").and_then(|x| x.as_str()),
6142                    d.get("Value").and_then(|x| x.as_str()),
6143                ) {
6144                    dimensions.insert(k.to_string(), v.to_string());
6145                }
6146            }
6147        }
6148
6149        let mut accounts = self.cloudwatch_state.write();
6150        let state = accounts.get_or_create(&self.account_id);
6151        let alarm_arn = format!(
6152            "arn:aws:cloudwatch:{}:{}:alarm:{}",
6153            self.region, self.account_id, alarm_name
6154        );
6155        let now = Utc::now();
6156        let alarm = MetricAlarm {
6157            alarm_name: alarm_name.clone(),
6158            alarm_arn: alarm_arn.clone(),
6159            alarm_description,
6160            actions_enabled,
6161            ok_actions,
6162            alarm_actions,
6163            insufficient_data_actions,
6164            state_value: AlarmState::InsufficientData,
6165            state_reason: "Unchecked: Initial alarm creation".to_string(),
6166            state_updated_timestamp: now,
6167            metric_name,
6168            namespace,
6169            statistic,
6170            extended_statistic,
6171            dimensions,
6172            period,
6173            unit,
6174            evaluation_periods,
6175            datapoints_to_alarm,
6176            threshold,
6177            comparison_operator,
6178            treat_missing_data,
6179            evaluate_low_sample_count_percentile,
6180            configuration_updated_timestamp: now,
6181            alarm_configuration_updated_timestamp: now,
6182        };
6183        let region_alarms = state.alarms_in_mut(&self.region);
6184        if region_alarms.contains_key(&alarm_name) {
6185            return Err(format!("Alarm {alarm_name} already exists"));
6186        }
6187        region_alarms.insert(alarm_name.clone(), alarm);
6188
6189        Ok(ProvisionResult::new(alarm_name).with("Arn", alarm_arn))
6190    }
6191
6192    fn delete_cloudwatch_alarm(&self, physical_id: &str) -> Result<(), String> {
6193        let mut accounts = self.cloudwatch_state.write();
6194        let state = accounts.get_or_create(&self.account_id);
6195        state.alarms_in_mut(&self.region).remove(physical_id);
6196        Ok(())
6197    }
6198
6199    fn create_cloudwatch_dashboard(
6200        &self,
6201        resource: &ResourceDefinition,
6202    ) -> Result<ProvisionResult, String> {
6203        let props = &resource.properties;
6204        let dashboard_name = props
6205            .get("DashboardName")
6206            .and_then(|v| v.as_str())
6207            .map(String::from)
6208            .unwrap_or_else(|| {
6209                let suffix = Uuid::new_v4().simple().to_string();
6210                format!("{}-{}", resource.logical_id, &suffix[..8])
6211            });
6212        // CFN passes DashboardBody as a JSON string (Fn::Sub friendly).
6213        let body = props
6214            .get("DashboardBody")
6215            .ok_or("DashboardBody is required")?;
6216        let body_str = if let Some(s) = body.as_str() {
6217            s.to_string()
6218        } else {
6219            serde_json::to_string(body).map_err(|e| format!("invalid DashboardBody: {e}"))?
6220        };
6221        // Validate JSON syntax to mirror real PutDashboard behavior.
6222        serde_json::from_str::<serde_json::Value>(&body_str)
6223            .map_err(|e| format!("DashboardBody must be valid JSON: {e}"))?;
6224
6225        let arn = format!(
6226            "arn:aws:cloudwatch::{}:dashboard/{dashboard_name}",
6227            self.account_id
6228        );
6229        let dashboard = Dashboard {
6230            name: dashboard_name.clone(),
6231            arn: arn.clone(),
6232            size_bytes: body_str.len() as i64,
6233            body: body_str,
6234            last_modified: Utc::now(),
6235        };
6236
6237        let mut accounts = self.cloudwatch_state.write();
6238        let state = accounts.get_or_create(&self.account_id);
6239        state.dashboards.insert(dashboard_name.clone(), dashboard);
6240
6241        Ok(ProvisionResult::new(dashboard_name).with("Arn", arn))
6242    }
6243
6244    fn delete_cloudwatch_dashboard(&self, physical_id: &str) -> Result<(), String> {
6245        let mut accounts = self.cloudwatch_state.write();
6246        let state = accounts.get_or_create(&self.account_id);
6247        state.dashboards.remove(physical_id);
6248        Ok(())
6249    }
6250
6251    fn update_cloudwatch_alarm(
6252        &self,
6253        existing: &StackResource,
6254        new_def: &ResourceDefinition,
6255    ) -> Result<ProvisionResult, String> {
6256        let props = &new_def.properties;
6257        // Renaming AlarmName forces replacement in real CFN; mirror that.
6258        let new_alarm_name = props
6259            .get("AlarmName")
6260            .and_then(|v| v.as_str())
6261            .unwrap_or(&new_def.logical_id);
6262        if new_alarm_name != existing.physical_id {
6263            return Err(
6264                "AWS::CloudWatch::Alarm updates that change AlarmName require replacement"
6265                    .to_string(),
6266            );
6267        }
6268
6269        let str_array = |key: &str| -> Vec<String> {
6270            props
6271                .get(key)
6272                .and_then(|v| v.as_array())
6273                .map(|arr| {
6274                    arr.iter()
6275                        .filter_map(|x| x.as_str().map(|s| s.to_string()))
6276                        .collect()
6277                })
6278                .unwrap_or_default()
6279        };
6280        let alarm_description = props
6281            .get("AlarmDescription")
6282            .and_then(|v| v.as_str())
6283            .map(|s| s.to_string());
6284        let actions_enabled = props
6285            .get("ActionsEnabled")
6286            .and_then(|v| v.as_bool())
6287            .unwrap_or(true);
6288        let alarm_actions = str_array("AlarmActions");
6289        let ok_actions = str_array("OKActions");
6290        let insufficient_data_actions = str_array("InsufficientDataActions");
6291        let metric_name = props
6292            .get("MetricName")
6293            .and_then(|v| v.as_str())
6294            .map(|s| s.to_string());
6295        let namespace = props
6296            .get("Namespace")
6297            .and_then(|v| v.as_str())
6298            .map(|s| s.to_string());
6299        let statistic = props
6300            .get("Statistic")
6301            .and_then(|v| v.as_str())
6302            .map(|s| s.to_string());
6303        let extended_statistic = props
6304            .get("ExtendedStatistic")
6305            .and_then(|v| v.as_str())
6306            .map(|s| s.to_string());
6307        let unit = props
6308            .get("Unit")
6309            .and_then(|v| v.as_str())
6310            .map(|s| s.to_string());
6311        let period = props.get("Period").and_then(|v| v.as_i64());
6312        let evaluation_periods = props
6313            .get("EvaluationPeriods")
6314            .and_then(|v| v.as_i64())
6315            .unwrap_or(1);
6316        let datapoints_to_alarm = props.get("DatapointsToAlarm").and_then(|v| v.as_i64());
6317        let threshold = props.get("Threshold").and_then(|v| v.as_f64());
6318        let comparison_operator = props
6319            .get("ComparisonOperator")
6320            .and_then(|v| v.as_str())
6321            .unwrap_or("GreaterThanThreshold")
6322            .to_string();
6323        let treat_missing_data = props
6324            .get("TreatMissingData")
6325            .and_then(|v| v.as_str())
6326            .map(|s| s.to_string());
6327        let evaluate_low_sample_count_percentile = props
6328            .get("EvaluateLowSampleCountPercentile")
6329            .and_then(|v| v.as_str())
6330            .map(|s| s.to_string());
6331
6332        let mut dimensions: BTreeMap<String, String> = BTreeMap::new();
6333        if let Some(arr) = props.get("Dimensions").and_then(|v| v.as_array()) {
6334            for d in arr {
6335                if let (Some(k), Some(v)) = (
6336                    d.get("Name").and_then(|x| x.as_str()),
6337                    d.get("Value").and_then(|x| x.as_str()),
6338                ) {
6339                    dimensions.insert(k.to_string(), v.to_string());
6340                }
6341            }
6342        }
6343
6344        let mut accounts = self.cloudwatch_state.write();
6345        let state = accounts.get_or_create(&self.account_id);
6346        let region_alarms = state.alarms_in_mut(&self.region);
6347        let alarm = region_alarms
6348            .get_mut(&existing.physical_id)
6349            .ok_or_else(|| format!("Alarm {} not found", existing.physical_id))?;
6350        let now = Utc::now();
6351        alarm.alarm_description = alarm_description;
6352        alarm.actions_enabled = actions_enabled;
6353        alarm.ok_actions = ok_actions;
6354        alarm.alarm_actions = alarm_actions;
6355        alarm.insufficient_data_actions = insufficient_data_actions;
6356        alarm.metric_name = metric_name;
6357        alarm.namespace = namespace;
6358        alarm.statistic = statistic;
6359        alarm.extended_statistic = extended_statistic;
6360        alarm.dimensions = dimensions;
6361        alarm.period = period;
6362        alarm.unit = unit;
6363        alarm.evaluation_periods = evaluation_periods;
6364        alarm.datapoints_to_alarm = datapoints_to_alarm;
6365        alarm.threshold = threshold;
6366        alarm.comparison_operator = comparison_operator;
6367        alarm.treat_missing_data = treat_missing_data;
6368        alarm.evaluate_low_sample_count_percentile = evaluate_low_sample_count_percentile;
6369        alarm.configuration_updated_timestamp = now;
6370        alarm.alarm_configuration_updated_timestamp = now;
6371
6372        let alarm_arn = alarm.alarm_arn.clone();
6373        Ok(ProvisionResult::new(existing.physical_id.clone()).with("Arn", alarm_arn))
6374    }
6375
6376    fn update_cloudwatch_dashboard(
6377        &self,
6378        existing: &StackResource,
6379        new_def: &ResourceDefinition,
6380    ) -> Result<ProvisionResult, String> {
6381        let props = &new_def.properties;
6382        // Renaming DashboardName forces replacement.
6383        if let Some(new_name) = props.get("DashboardName").and_then(|v| v.as_str()) {
6384            if new_name != existing.physical_id {
6385                return Err(
6386                    "AWS::CloudWatch::Dashboard updates that change DashboardName require replacement"
6387                        .to_string(),
6388                );
6389            }
6390        }
6391        let body = props
6392            .get("DashboardBody")
6393            .ok_or("DashboardBody is required")?;
6394        let body_str = if let Some(s) = body.as_str() {
6395            s.to_string()
6396        } else {
6397            serde_json::to_string(body).map_err(|e| format!("invalid DashboardBody: {e}"))?
6398        };
6399        serde_json::from_str::<serde_json::Value>(&body_str)
6400            .map_err(|e| format!("DashboardBody must be valid JSON: {e}"))?;
6401
6402        let mut accounts = self.cloudwatch_state.write();
6403        let state = accounts.get_or_create(&self.account_id);
6404        let dashboard = state
6405            .dashboards
6406            .get_mut(&existing.physical_id)
6407            .ok_or_else(|| format!("Dashboard {} not found", existing.physical_id))?;
6408        dashboard.size_bytes = body_str.len() as i64;
6409        dashboard.body = body_str;
6410        dashboard.last_modified = Utc::now();
6411        let arn = dashboard.arn.clone();
6412        Ok(ProvisionResult::new(existing.physical_id.clone()).with("Arn", arn))
6413    }
6414
6415    // --- ELBv2 ---
6416
6417    fn create_elbv2_load_balancer(
6418        &self,
6419        resource: &ResourceDefinition,
6420    ) -> Result<ProvisionResult, String> {
6421        let props = &resource.properties;
6422        let name = props
6423            .get("Name")
6424            .and_then(|v| v.as_str())
6425            .unwrap_or(&resource.logical_id)
6426            .to_string();
6427        let scheme = props
6428            .get("Scheme")
6429            .and_then(|v| v.as_str())
6430            .unwrap_or("internet-facing")
6431            .to_string();
6432        let lb_type = props
6433            .get("Type")
6434            .and_then(|v| v.as_str())
6435            .unwrap_or("application")
6436            .to_string();
6437        let ip_address_type = props
6438            .get("IpAddressType")
6439            .and_then(|v| v.as_str())
6440            .unwrap_or("ipv4")
6441            .to_string();
6442        let security_groups: Vec<String> = props
6443            .get("SecurityGroups")
6444            .and_then(|v| v.as_array())
6445            .map(|arr| {
6446                arr.iter()
6447                    .filter_map(|s| s.as_str().map(|s| s.to_string()))
6448                    .collect()
6449            })
6450            .unwrap_or_default();
6451        let tags = parse_elb_tags(props.get("Tags"));
6452
6453        let mut accounts = self.elbv2_state.write();
6454        let state = accounts.get_or_create(&self.account_id);
6455        let lb_id = Uuid::new_v4().simple().to_string();
6456        let arn = format!(
6457            "arn:aws:elasticloadbalancing:{}:{}:loadbalancer/{}/{}/{}",
6458            self.region,
6459            self.account_id,
6460            if lb_type == "network" { "net" } else { "app" },
6461            name,
6462            &lb_id[..16]
6463        );
6464        let dns_name = format!(
6465            "{}-{}.{}.elb.{}.amazonaws.com",
6466            name,
6467            &lb_id[..16],
6468            self.region,
6469            self.region
6470        );
6471
6472        let mut availability_zones: Vec<fakecloud_elbv2::AvailabilityZone> = Vec::new();
6473        if let Some(arr) = props.get("Subnets").and_then(|v| v.as_array()) {
6474            for s in arr {
6475                if let Some(subnet_id) = s.as_str() {
6476                    availability_zones.push(fakecloud_elbv2::AvailabilityZone {
6477                        zone_name: format!("{}a", self.region),
6478                        subnet_id: subnet_id.to_string(),
6479                        outpost_id: None,
6480                        load_balancer_addresses: Vec::new(),
6481                        source_nat_ipv6_prefixes: Vec::new(),
6482                    });
6483                }
6484            }
6485        }
6486
6487        state.load_balancers.insert(
6488            arn.clone(),
6489            LoadBalancer {
6490                arn: arn.clone(),
6491                name: name.clone(),
6492                dns_name: dns_name.clone(),
6493                canonical_hosted_zone_id: "Z2P70J7EXAMPLE".to_string(),
6494                created_time: Utc::now(),
6495                scheme,
6496                vpc_id: String::new(),
6497                state_code: "active".to_string(),
6498                state_reason: None,
6499                lb_type,
6500                availability_zones,
6501                security_groups,
6502                ip_address_type,
6503                customer_owned_ipv4_pool: None,
6504                enforce_security_group_inbound_rules_on_private_link_traffic: None,
6505                enable_prefix_for_ipv6_source_nat: None,
6506                ipv4_ipam_pool_id: None,
6507                tags,
6508                attributes: BTreeMap::new(),
6509                minimum_capacity_units: None,
6510                bound_port: None,
6511            },
6512        );
6513
6514        Ok(ProvisionResult::new(arn.clone())
6515            .with("LoadBalancerArn", arn)
6516            .with(
6517                "LoadBalancerFullName",
6518                format!("app/{name}/{}", &lb_id[..16]),
6519            )
6520            .with("LoadBalancerName", name)
6521            .with("DNSName", dns_name)
6522            .with("CanonicalHostedZoneID", "Z2P70J7EXAMPLE"))
6523    }
6524
6525    fn delete_elbv2_load_balancer(&self, physical_id: &str) -> Result<(), String> {
6526        let mut accounts = self.elbv2_state.write();
6527        let state = accounts.get_or_create(&self.account_id);
6528        state.load_balancers.remove(physical_id);
6529        // Cascade-delete listeners and rules attached to this LB.
6530        let listeners: Vec<String> = state
6531            .listeners
6532            .iter()
6533            .filter(|(_, l)| l.load_balancer_arn == physical_id)
6534            .map(|(arn, _)| arn.clone())
6535            .collect();
6536        for arn in &listeners {
6537            state.listeners.remove(arn);
6538            let rules: Vec<String> = state
6539                .rules
6540                .iter()
6541                .filter(|(_, r)| r.listener_arn == *arn)
6542                .map(|(a, _)| a.clone())
6543                .collect();
6544            for r in rules {
6545                state.rules.remove(&r);
6546            }
6547        }
6548        for tg in state.target_groups.values_mut() {
6549            tg.load_balancer_arns.retain(|a| a != physical_id);
6550        }
6551        Ok(())
6552    }
6553
6554    fn create_elbv2_target_group(
6555        &self,
6556        resource: &ResourceDefinition,
6557    ) -> Result<ProvisionResult, String> {
6558        let props = &resource.properties;
6559        let name = props
6560            .get("Name")
6561            .and_then(|v| v.as_str())
6562            .unwrap_or(&resource.logical_id)
6563            .to_string();
6564        let protocol = props
6565            .get("Protocol")
6566            .and_then(|v| v.as_str())
6567            .map(|s| s.to_string());
6568        let port = props.get("Port").and_then(|v| v.as_i64()).map(|n| n as i32);
6569        let vpc_id = props
6570            .get("VpcId")
6571            .and_then(|v| v.as_str())
6572            .map(|s| s.to_string());
6573        let target_type = props
6574            .get("TargetType")
6575            .and_then(|v| v.as_str())
6576            .unwrap_or("instance")
6577            .to_string();
6578        let ip_address_type = props
6579            .get("IpAddressType")
6580            .and_then(|v| v.as_str())
6581            .unwrap_or("ipv4")
6582            .to_string();
6583        let protocol_version = props
6584            .get("ProtocolVersion")
6585            .and_then(|v| v.as_str())
6586            .map(|s| s.to_string());
6587        let tags = parse_elb_tags(props.get("Tags"));
6588
6589        let mut accounts = self.elbv2_state.write();
6590        let state = accounts.get_or_create(&self.account_id);
6591        let id = Uuid::new_v4().simple().to_string();
6592        let arn = format!(
6593            "arn:aws:elasticloadbalancing:{}:{}:targetgroup/{}/{}",
6594            self.region,
6595            self.account_id,
6596            name,
6597            &id[..16]
6598        );
6599
6600        state.target_groups.insert(
6601            arn.clone(),
6602            TargetGroup {
6603                arn: arn.clone(),
6604                name: name.clone(),
6605                protocol,
6606                port,
6607                vpc_id,
6608                target_type,
6609                ip_address_type,
6610                protocol_version,
6611                health_check_protocol: props
6612                    .get("HealthCheckProtocol")
6613                    .and_then(|v| v.as_str())
6614                    .map(|s| s.to_string()),
6615                health_check_port: props
6616                    .get("HealthCheckPort")
6617                    .and_then(|v| v.as_str())
6618                    .map(|s| s.to_string()),
6619                health_check_enabled: props
6620                    .get("HealthCheckEnabled")
6621                    .and_then(|v| v.as_bool())
6622                    .unwrap_or(true),
6623                health_check_path: props
6624                    .get("HealthCheckPath")
6625                    .and_then(|v| v.as_str())
6626                    .map(|s| s.to_string()),
6627                health_check_interval_seconds: props
6628                    .get("HealthCheckIntervalSeconds")
6629                    .and_then(|v| v.as_i64())
6630                    .unwrap_or(30) as i32,
6631                health_check_timeout_seconds: props
6632                    .get("HealthCheckTimeoutSeconds")
6633                    .and_then(|v| v.as_i64())
6634                    .unwrap_or(5) as i32,
6635                healthy_threshold_count: props
6636                    .get("HealthyThresholdCount")
6637                    .and_then(|v| v.as_i64())
6638                    .unwrap_or(5) as i32,
6639                unhealthy_threshold_count: props
6640                    .get("UnhealthyThresholdCount")
6641                    .and_then(|v| v.as_i64())
6642                    .unwrap_or(2) as i32,
6643                matcher_http_code: props
6644                    .get("Matcher")
6645                    .and_then(|v| v.get("HttpCode"))
6646                    .and_then(|v| v.as_str())
6647                    .map(|s| s.to_string()),
6648                matcher_grpc_code: props
6649                    .get("Matcher")
6650                    .and_then(|v| v.get("GrpcCode"))
6651                    .and_then(|v| v.as_str())
6652                    .map(|s| s.to_string()),
6653                load_balancer_arns: Vec::new(),
6654                targets: Vec::new(),
6655                tags,
6656                attributes: BTreeMap::new(),
6657                created_time: Utc::now(),
6658            },
6659        );
6660
6661        Ok(ProvisionResult::new(arn.clone())
6662            .with("TargetGroupArn", arn)
6663            .with("TargetGroupName", name)
6664            .with("TargetGroupFullName", format!("targetgroup/{}", &id[..16])))
6665    }
6666
6667    fn delete_elbv2_target_group(&self, physical_id: &str) -> Result<(), String> {
6668        let mut accounts = self.elbv2_state.write();
6669        let state = accounts.get_or_create(&self.account_id);
6670        state.target_groups.remove(physical_id);
6671        Ok(())
6672    }
6673
6674    fn create_elbv2_listener(
6675        &self,
6676        resource: &ResourceDefinition,
6677    ) -> Result<ProvisionResult, String> {
6678        let props = &resource.properties;
6679        let load_balancer_arn = props
6680            .get("LoadBalancerArn")
6681            .and_then(|v| v.as_str())
6682            .ok_or_else(|| "LoadBalancerArn is required".to_string())?
6683            .to_string();
6684        let port = props.get("Port").and_then(|v| v.as_i64()).map(|n| n as i32);
6685        let protocol = props
6686            .get("Protocol")
6687            .and_then(|v| v.as_str())
6688            .map(|s| s.to_string());
6689        let default_actions = parse_elb_actions(props.get("DefaultActions"));
6690
6691        let mut accounts = self.elbv2_state.write();
6692        let state = accounts.get_or_create(&self.account_id);
6693        if !state.load_balancers.contains_key(&load_balancer_arn) {
6694            return Err(format!(
6695                "LoadBalancer {load_balancer_arn} not yet provisioned"
6696            ));
6697        }
6698
6699        let lb_full = load_balancer_arn
6700            .rsplit("loadbalancer/")
6701            .next()
6702            .unwrap_or("")
6703            .to_string();
6704        let listener_id = Uuid::new_v4().simple().to_string();
6705        let arn = format!(
6706            "arn:aws:elasticloadbalancing:{}:{}:listener/{}/{}",
6707            self.region,
6708            self.account_id,
6709            lb_full,
6710            &listener_id[..16]
6711        );
6712
6713        // Wire forward target groups -> LB association so dataplane probing
6714        // and DescribeTargetGroups round-trip the relationship.
6715        for action in &default_actions {
6716            if let Some(tg_arn) = &action.target_group_arn {
6717                if let Some(tg) = state.target_groups.get_mut(tg_arn) {
6718                    if !tg.load_balancer_arns.contains(&load_balancer_arn) {
6719                        tg.load_balancer_arns.push(load_balancer_arn.clone());
6720                    }
6721                }
6722            }
6723            if let Some(forward) = &action.forward {
6724                for tgt in &forward.target_groups {
6725                    if let Some(tg) = state.target_groups.get_mut(&tgt.target_group_arn) {
6726                        if !tg.load_balancer_arns.contains(&load_balancer_arn) {
6727                            tg.load_balancer_arns.push(load_balancer_arn.clone());
6728                        }
6729                    }
6730                }
6731            }
6732        }
6733
6734        state.listeners.insert(
6735            arn.clone(),
6736            Listener {
6737                arn: arn.clone(),
6738                load_balancer_arn,
6739                port,
6740                protocol,
6741                certificates: Vec::new(),
6742                ssl_policy: props
6743                    .get("SslPolicy")
6744                    .and_then(|v| v.as_str())
6745                    .map(|s| s.to_string()),
6746                default_actions,
6747                alpn_policy: Vec::new(),
6748                mutual_authentication: None,
6749                tags: parse_elb_tags(props.get("Tags")),
6750                attributes: BTreeMap::new(),
6751            },
6752        );
6753
6754        Ok(ProvisionResult::new(arn.clone()).with("ListenerArn", arn))
6755    }
6756
6757    fn delete_elbv2_listener(&self, physical_id: &str) -> Result<(), String> {
6758        let mut accounts = self.elbv2_state.write();
6759        let state = accounts.get_or_create(&self.account_id);
6760        state.listeners.remove(physical_id);
6761        let rules: Vec<String> = state
6762            .rules
6763            .iter()
6764            .filter(|(_, r)| r.listener_arn == physical_id)
6765            .map(|(arn, _)| arn.clone())
6766            .collect();
6767        for r in rules {
6768            state.rules.remove(&r);
6769        }
6770        Ok(())
6771    }
6772
6773    fn create_elbv2_listener_rule(
6774        &self,
6775        resource: &ResourceDefinition,
6776    ) -> Result<ProvisionResult, String> {
6777        let props = &resource.properties;
6778        let listener_arn = props
6779            .get("ListenerArn")
6780            .and_then(|v| v.as_str())
6781            .ok_or_else(|| "ListenerArn is required".to_string())?
6782            .to_string();
6783        let priority = props
6784            .get("Priority")
6785            .map(|v| {
6786                if let Some(s) = v.as_str() {
6787                    s.to_string()
6788                } else if let Some(n) = v.as_i64() {
6789                    n.to_string()
6790                } else {
6791                    "1".to_string()
6792                }
6793            })
6794            .unwrap_or_else(|| "1".to_string());
6795        let actions = parse_elb_actions(props.get("Actions"));
6796        let conditions = parse_elb_rule_conditions(props.get("Conditions"));
6797
6798        let mut accounts = self.elbv2_state.write();
6799        let state = accounts.get_or_create(&self.account_id);
6800        if !state.listeners.contains_key(&listener_arn) {
6801            return Err(format!("Listener {listener_arn} not yet provisioned"));
6802        }
6803        let listener_full = listener_arn
6804            .rsplit("listener/")
6805            .next()
6806            .unwrap_or("")
6807            .to_string();
6808        let rule_id = Uuid::new_v4().simple().to_string();
6809        let arn = format!(
6810            "arn:aws:elasticloadbalancing:{}:{}:listener-rule/{}/{}",
6811            self.region,
6812            self.account_id,
6813            listener_full,
6814            &rule_id[..16]
6815        );
6816
6817        state.rules.insert(
6818            arn.clone(),
6819            ElbRule {
6820                arn: arn.clone(),
6821                listener_arn,
6822                priority,
6823                conditions,
6824                actions,
6825                is_default: false,
6826                tags: parse_elb_tags(props.get("Tags")),
6827            },
6828        );
6829
6830        Ok(ProvisionResult::new(arn.clone()).with("RuleArn", arn))
6831    }
6832
6833    fn delete_elbv2_listener_rule(&self, physical_id: &str) -> Result<(), String> {
6834        let mut accounts = self.elbv2_state.write();
6835        let state = accounts.get_or_create(&self.account_id);
6836        state.rules.remove(physical_id);
6837        Ok(())
6838    }
6839
6840    /// Provision an `AWS::ElasticLoadBalancingV2::ListenerCertificate`.
6841    /// Appends each non-default certificate from `Certificates` to the
6842    /// target listener (the default listener cert is set on Listener
6843    /// creation, so this resource only manages SNI extras).
6844    fn create_elbv2_listener_certificate(
6845        &self,
6846        resource: &ResourceDefinition,
6847    ) -> Result<ProvisionResult, String> {
6848        let props = &resource.properties;
6849        let listener_arn = props
6850            .get("ListenerArn")
6851            .and_then(|v| v.as_str())
6852            .ok_or_else(|| "ListenerArn is required".to_string())?
6853            .to_string();
6854        let certs: Vec<String> = props
6855            .get("Certificates")
6856            .and_then(|v| v.as_array())
6857            .map(|arr| {
6858                arr.iter()
6859                    .filter_map(|c| c.get("CertificateArn").and_then(|v| v.as_str()))
6860                    .map(|s| s.to_string())
6861                    .collect()
6862            })
6863            .unwrap_or_default();
6864        if certs.is_empty() {
6865            return Err("Certificates must contain at least one CertificateArn".to_string());
6866        }
6867        let mut accounts = self.elbv2_state.write();
6868        let state = accounts.get_or_create(&self.account_id);
6869        let listener = state
6870            .listeners
6871            .get_mut(&listener_arn)
6872            .ok_or_else(|| format!("Listener {listener_arn} does not exist"))?;
6873        for arn in &certs {
6874            listener.certificates.retain(|c| &c.certificate_arn != arn);
6875            listener.certificates.push(fakecloud_elbv2::Certificate {
6876                certificate_arn: arn.clone(),
6877                is_default: false,
6878            });
6879        }
6880        Ok(ProvisionResult::new(format!(
6881            "{}#{}",
6882            listener_arn,
6883            certs.join(",")
6884        )))
6885    }
6886
6887    fn delete_elbv2_listener_certificate(&self, physical_id: &str) -> Result<(), String> {
6888        let (listener_arn, cert_list) = match physical_id.split_once('#') {
6889            Some(parts) => parts,
6890            None => return Ok(()),
6891        };
6892        let cert_arns: Vec<&str> = cert_list.split(',').collect();
6893        let mut accounts = self.elbv2_state.write();
6894        let state = accounts.get_or_create(&self.account_id);
6895        if let Some(listener) = state.listeners.get_mut(listener_arn) {
6896            listener
6897                .certificates
6898                .retain(|c| !cert_arns.iter().any(|a| *a == c.certificate_arn));
6899        }
6900        Ok(())
6901    }
6902
6903    /// Provision an `AWS::ElasticLoadBalancingV2::TrustStore`.
6904    fn create_elbv2_trust_store(
6905        &self,
6906        resource: &ResourceDefinition,
6907    ) -> Result<ProvisionResult, String> {
6908        let props = &resource.properties;
6909        let name = props
6910            .get("Name")
6911            .and_then(|v| v.as_str())
6912            .unwrap_or(&resource.logical_id)
6913            .to_string();
6914        let bucket = props
6915            .get("CaCertificatesBundleS3Bucket")
6916            .and_then(|v| v.as_str())
6917            .ok_or_else(|| "CaCertificatesBundleS3Bucket is required".to_string())?;
6918        let key = props
6919            .get("CaCertificatesBundleS3Key")
6920            .and_then(|v| v.as_str())
6921            .ok_or_else(|| "CaCertificatesBundleS3Key is required".to_string())?;
6922        let tags: Vec<fakecloud_elbv2::Tag> = props
6923            .get("Tags")
6924            .and_then(|v| v.as_array())
6925            .map(|arr| {
6926                arr.iter()
6927                    .filter_map(|t| {
6928                        let k = t.get("Key").and_then(|v| v.as_str())?;
6929                        let val = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
6930                        Some(fakecloud_elbv2::Tag {
6931                            key: k.to_string(),
6932                            value: val.to_string(),
6933                        })
6934                    })
6935                    .collect()
6936            })
6937            .unwrap_or_default();
6938
6939        let mut accounts = self.elbv2_state.write();
6940        let state = accounts.get_or_create(&self.account_id);
6941        if state.trust_stores.values().any(|t| t.name == name) {
6942            return Err(format!("Trust store {name} already exists"));
6943        }
6944        let suffix: String = Uuid::new_v4()
6945            .simple()
6946            .to_string()
6947            .chars()
6948            .take(16)
6949            .collect();
6950        let arn = format!(
6951            "arn:aws:elasticloadbalancing:{}:{}:truststore/{}/{}",
6952            self.region, self.account_id, name, suffix
6953        );
6954        let ts = fakecloud_elbv2::TrustStore {
6955            arn: arn.clone(),
6956            name: name.clone(),
6957            status: "ACTIVE".to_string(),
6958            number_of_ca_certificates: 1,
6959            total_revoked_entries: 0,
6960            created_time: Utc::now(),
6961            ca_certificates_bundle: Some(format!("s3://{bucket}/{key}").into_bytes()),
6962            revocations: BTreeMap::new(),
6963            next_revocation_id: 1,
6964            tags,
6965        };
6966        state.trust_stores.insert(arn.clone(), ts);
6967        Ok(ProvisionResult::new(arn.clone())
6968            .with("TrustStoreArn", arn)
6969            .with("Name", name)
6970            .with("Status", "ACTIVE".to_string()))
6971    }
6972
6973    fn delete_elbv2_trust_store(&self, physical_id: &str) -> Result<(), String> {
6974        let mut accounts = self.elbv2_state.write();
6975        let state = accounts.get_or_create(&self.account_id);
6976        state.trust_stores.remove(physical_id);
6977        Ok(())
6978    }
6979
6980    /// In-place update for AWS::ElasticLoadBalancingV2::LoadBalancer. Name,
6981    /// scheme, type and subnet topology are immutable in real AWS — CFN
6982    /// would replace the resource. We only mutate fields the SetSubnets /
6983    /// SetSecurityGroups / SetIpAddressType APIs would touch.
6984    fn update_elbv2_load_balancer(
6985        &self,
6986        existing: &StackResource,
6987        resource: &ResourceDefinition,
6988    ) -> Result<ProvisionResult, String> {
6989        let props = &resource.properties;
6990        let arn = existing.physical_id.clone();
6991        let mut accounts = self.elbv2_state.write();
6992        let state = accounts.get_or_create(&self.account_id);
6993        let lb = state
6994            .load_balancers
6995            .get_mut(&arn)
6996            .ok_or_else(|| format!("LoadBalancer {arn} no longer exists"))?;
6997        if let Some(arr) = props.get("SecurityGroups").and_then(|v| v.as_array()) {
6998            lb.security_groups = arr
6999                .iter()
7000                .filter_map(|s| s.as_str().map(|s| s.to_string()))
7001                .collect();
7002        }
7003        if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
7004            lb.ip_address_type = s.to_string();
7005        }
7006        if let Some(arr) = props.get("Subnets").and_then(|v| v.as_array()) {
7007            let mut zones: Vec<fakecloud_elbv2::AvailabilityZone> = Vec::new();
7008            for s in arr {
7009                if let Some(subnet_id) = s.as_str() {
7010                    zones.push(fakecloud_elbv2::AvailabilityZone {
7011                        zone_name: format!("{}a", self.region),
7012                        subnet_id: subnet_id.to_string(),
7013                        outpost_id: None,
7014                        load_balancer_addresses: Vec::new(),
7015                        source_nat_ipv6_prefixes: Vec::new(),
7016                    });
7017                }
7018            }
7019            lb.availability_zones = zones;
7020        }
7021        if props.get("Tags").is_some() {
7022            lb.tags = parse_elb_tags(props.get("Tags"));
7023        }
7024        let name = lb.name.clone();
7025        let dns_name = lb.dns_name.clone();
7026        let canonical = lb.canonical_hosted_zone_id.clone();
7027        let lb_full = arn.rsplit("loadbalancer/").next().unwrap_or("").to_string();
7028        Ok(ProvisionResult::new(arn.clone())
7029            .with("LoadBalancerArn", arn)
7030            .with("LoadBalancerFullName", lb_full)
7031            .with("LoadBalancerName", name)
7032            .with("DNSName", dns_name)
7033            .with("CanonicalHostedZoneID", canonical))
7034    }
7035
7036    /// In-place update for AWS::ElasticLoadBalancingV2::TargetGroup. Mirrors
7037    /// ModifyTargetGroup: only health-check fields and matcher are mutable.
7038    fn update_elbv2_target_group(
7039        &self,
7040        existing: &StackResource,
7041        resource: &ResourceDefinition,
7042    ) -> Result<ProvisionResult, String> {
7043        let props = &resource.properties;
7044        let arn = existing.physical_id.clone();
7045        let mut accounts = self.elbv2_state.write();
7046        let state = accounts.get_or_create(&self.account_id);
7047        let tg = state
7048            .target_groups
7049            .get_mut(&arn)
7050            .ok_or_else(|| format!("TargetGroup {arn} no longer exists"))?;
7051        if let Some(s) = props.get("HealthCheckProtocol").and_then(|v| v.as_str()) {
7052            tg.health_check_protocol = Some(s.to_string());
7053        }
7054        if let Some(s) = props.get("HealthCheckPort").and_then(|v| v.as_str()) {
7055            tg.health_check_port = Some(s.to_string());
7056        }
7057        if let Some(b) = props.get("HealthCheckEnabled").and_then(|v| v.as_bool()) {
7058            tg.health_check_enabled = b;
7059        }
7060        if let Some(s) = props.get("HealthCheckPath").and_then(|v| v.as_str()) {
7061            tg.health_check_path = Some(s.to_string());
7062        }
7063        if let Some(n) = props.get("HealthCheckIntervalSeconds").and_then(cfn_as_i64) {
7064            tg.health_check_interval_seconds = n as i32;
7065        }
7066        if let Some(n) = props.get("HealthCheckTimeoutSeconds").and_then(cfn_as_i64) {
7067            tg.health_check_timeout_seconds = n as i32;
7068        }
7069        if let Some(n) = props.get("HealthyThresholdCount").and_then(cfn_as_i64) {
7070            tg.healthy_threshold_count = n as i32;
7071        }
7072        if let Some(n) = props.get("UnhealthyThresholdCount").and_then(cfn_as_i64) {
7073            tg.unhealthy_threshold_count = n as i32;
7074        }
7075        if let Some(matcher) = props.get("Matcher") {
7076            tg.matcher_http_code = matcher
7077                .get("HttpCode")
7078                .and_then(|v| v.as_str())
7079                .map(|s| s.to_string());
7080            tg.matcher_grpc_code = matcher
7081                .get("GrpcCode")
7082                .and_then(|v| v.as_str())
7083                .map(|s| s.to_string());
7084        }
7085        if props.get("Tags").is_some() {
7086            tg.tags = parse_elb_tags(props.get("Tags"));
7087        }
7088        let name = tg.name.clone();
7089        let tg_full = arn
7090            .rsplit("targetgroup/")
7091            .next()
7092            .map(|s| format!("targetgroup/{s}"))
7093            .unwrap_or_default();
7094        Ok(ProvisionResult::new(arn.clone())
7095            .with("TargetGroupArn", arn)
7096            .with("TargetGroupName", name)
7097            .with("TargetGroupFullName", tg_full))
7098    }
7099
7100    /// In-place update for AWS::ElasticLoadBalancingV2::Listener. Mirrors
7101    /// ModifyListener: port, protocol, default actions, certs, ssl policy.
7102    fn update_elbv2_listener(
7103        &self,
7104        existing: &StackResource,
7105        resource: &ResourceDefinition,
7106    ) -> Result<ProvisionResult, String> {
7107        let props = &resource.properties;
7108        let arn = existing.physical_id.clone();
7109        let new_default_actions = props
7110            .get("DefaultActions")
7111            .map(|v| parse_elb_actions(Some(v)));
7112        let mut accounts = self.elbv2_state.write();
7113        let state = accounts.get_or_create(&self.account_id);
7114        let listener = state
7115            .listeners
7116            .get_mut(&arn)
7117            .ok_or_else(|| format!("Listener {arn} no longer exists"))?;
7118        if let Some(n) = props.get("Port").and_then(cfn_as_i64) {
7119            listener.port = Some(n as i32);
7120        }
7121        if let Some(s) = props.get("Protocol").and_then(|v| v.as_str()) {
7122            listener.protocol = Some(s.to_string());
7123        }
7124        if let Some(s) = props.get("SslPolicy").and_then(|v| v.as_str()) {
7125            listener.ssl_policy = Some(s.to_string());
7126        }
7127        if let Some(actions) = new_default_actions {
7128            listener.default_actions = actions;
7129        }
7130        if props.get("Tags").is_some() {
7131            listener.tags = parse_elb_tags(props.get("Tags"));
7132        }
7133        Ok(ProvisionResult::new(arn.clone()).with("ListenerArn", arn))
7134    }
7135
7136    /// In-place update for AWS::ElasticLoadBalancingV2::ListenerRule. Mirrors
7137    /// ModifyRule + SetRulePriorities.
7138    fn update_elbv2_listener_rule(
7139        &self,
7140        existing: &StackResource,
7141        resource: &ResourceDefinition,
7142    ) -> Result<ProvisionResult, String> {
7143        let props = &resource.properties;
7144        let arn = existing.physical_id.clone();
7145        let new_actions = props.get("Actions").map(|v| parse_elb_actions(Some(v)));
7146        let new_conditions = props
7147            .get("Conditions")
7148            .map(|v| parse_elb_rule_conditions(Some(v)));
7149        let mut accounts = self.elbv2_state.write();
7150        let state = accounts.get_or_create(&self.account_id);
7151        let rule = state
7152            .rules
7153            .get_mut(&arn)
7154            .ok_or_else(|| format!("ListenerRule {arn} no longer exists"))?;
7155        if let Some(v) = props.get("Priority") {
7156            rule.priority = if let Some(s) = v.as_str() {
7157                s.to_string()
7158            } else if let Some(n) = v.as_i64() {
7159                n.to_string()
7160            } else {
7161                rule.priority.clone()
7162            };
7163        }
7164        if let Some(actions) = new_actions {
7165            rule.actions = actions;
7166        }
7167        if let Some(conditions) = new_conditions {
7168            rule.conditions = conditions;
7169        }
7170        if props.get("Tags").is_some() {
7171            rule.tags = parse_elb_tags(props.get("Tags"));
7172        }
7173        Ok(ProvisionResult::new(arn.clone()).with("RuleArn", arn))
7174    }
7175
7176    /// In-place update for AWS::ElasticLoadBalancingV2::ListenerCertificate.
7177    /// CFN treats this as replace-on-cert-list-change in real AWS, but we
7178    /// can rebuild the SNI cert set against the same physical id without
7179    /// disrupting the listener.
7180    fn update_elbv2_listener_certificate(
7181        &self,
7182        existing: &StackResource,
7183        resource: &ResourceDefinition,
7184    ) -> Result<ProvisionResult, String> {
7185        let props = &resource.properties;
7186        let physical_id = existing.physical_id.clone();
7187        let listener_arn = props
7188            .get("ListenerArn")
7189            .and_then(|v| v.as_str())
7190            .map(|s| s.to_string())
7191            .or_else(|| physical_id.split_once('#').map(|(l, _)| l.to_string()))
7192            .ok_or_else(|| "ListenerArn is required".to_string())?;
7193        let new_certs: Vec<String> = props
7194            .get("Certificates")
7195            .and_then(|v| v.as_array())
7196            .map(|arr| {
7197                arr.iter()
7198                    .filter_map(|c| c.get("CertificateArn").and_then(|v| v.as_str()))
7199                    .map(|s| s.to_string())
7200                    .collect()
7201            })
7202            .unwrap_or_default();
7203        if new_certs.is_empty() {
7204            return Err("Certificates must contain at least one CertificateArn".to_string());
7205        }
7206
7207        // Strip the previously-managed certs, then attach the new set.
7208        let prev_certs: Vec<String> = physical_id
7209            .split_once('#')
7210            .map(|(_, list)| list.split(',').map(|s| s.to_string()).collect())
7211            .unwrap_or_default();
7212
7213        let mut accounts = self.elbv2_state.write();
7214        let state = accounts.get_or_create(&self.account_id);
7215        let listener = state
7216            .listeners
7217            .get_mut(&listener_arn)
7218            .ok_or_else(|| format!("Listener {listener_arn} does not exist"))?;
7219        listener
7220            .certificates
7221            .retain(|c| !prev_certs.iter().any(|p| p == &c.certificate_arn));
7222        for arn in &new_certs {
7223            listener.certificates.retain(|c| &c.certificate_arn != arn);
7224            listener.certificates.push(fakecloud_elbv2::Certificate {
7225                certificate_arn: arn.clone(),
7226                is_default: false,
7227            });
7228        }
7229        Ok(ProvisionResult::new(format!(
7230            "{}#{}",
7231            listener_arn,
7232            new_certs.join(",")
7233        )))
7234    }
7235
7236    /// In-place update for AWS::ElasticLoadBalancingV2::TrustStore. Only the
7237    /// CA bundle and tags are mutable; name is immutable in real AWS.
7238    fn update_elbv2_trust_store(
7239        &self,
7240        existing: &StackResource,
7241        resource: &ResourceDefinition,
7242    ) -> Result<ProvisionResult, String> {
7243        let props = &resource.properties;
7244        let arn = existing.physical_id.clone();
7245        let mut accounts = self.elbv2_state.write();
7246        let state = accounts.get_or_create(&self.account_id);
7247        let ts = state
7248            .trust_stores
7249            .get_mut(&arn)
7250            .ok_or_else(|| format!("TrustStore {arn} no longer exists"))?;
7251        let new_bucket = props
7252            .get("CaCertificatesBundleS3Bucket")
7253            .and_then(|v| v.as_str());
7254        let new_key = props
7255            .get("CaCertificatesBundleS3Key")
7256            .and_then(|v| v.as_str());
7257        if let (Some(b), Some(k)) = (new_bucket, new_key) {
7258            ts.ca_certificates_bundle = Some(format!("s3://{b}/{k}").into_bytes());
7259        }
7260        if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
7261            ts.tags = arr
7262                .iter()
7263                .filter_map(|t| {
7264                    let k = t.get("Key").and_then(|v| v.as_str())?;
7265                    let v = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
7266                    Some(fakecloud_elbv2::Tag {
7267                        key: k.to_string(),
7268                        value: v.to_string(),
7269                    })
7270                })
7271                .collect();
7272        }
7273        let name = ts.name.clone();
7274        let status = ts.status.clone();
7275        Ok(ProvisionResult::new(arn.clone())
7276            .with("TrustStoreArn", arn)
7277            .with("Name", name)
7278            .with("Status", status))
7279    }
7280
7281    /// Live-state GetAtt fallback for AWS::ElasticLoadBalancingV2::LoadBalancer.
7282    fn get_att_elbv2_load_balancer(&self, physical_id: &str, attribute: &str) -> Option<String> {
7283        let mut accounts = self.elbv2_state.write();
7284        let state = accounts.get_or_create(&self.account_id);
7285        let lb = state.load_balancers.get(physical_id)?;
7286        let lb_full = lb
7287            .arn
7288            .rsplit("loadbalancer/")
7289            .next()
7290            .unwrap_or("")
7291            .to_string();
7292        match attribute {
7293            "Arn" | "LoadBalancerArn" => Some(lb.arn.clone()),
7294            "DNSName" => Some(lb.dns_name.clone()),
7295            "CanonicalHostedZoneID" => Some(lb.canonical_hosted_zone_id.clone()),
7296            "LoadBalancerFullName" => Some(lb_full),
7297            "LoadBalancerName" => Some(lb.name.clone()),
7298            "SecurityGroups" => Some(lb.security_groups.join(",")),
7299            _ => None,
7300        }
7301    }
7302
7303    /// Live-state GetAtt fallback for AWS::ElasticLoadBalancingV2::TargetGroup.
7304    fn get_att_elbv2_target_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
7305        let mut accounts = self.elbv2_state.write();
7306        let state = accounts.get_or_create(&self.account_id);
7307        let tg = state.target_groups.get(physical_id)?;
7308        let tg_full = tg
7309            .arn
7310            .rsplit("targetgroup/")
7311            .next()
7312            .map(|s| format!("targetgroup/{s}"))
7313            .unwrap_or_default();
7314        match attribute {
7315            "TargetGroupArn" => Some(tg.arn.clone()),
7316            "TargetGroupName" => Some(tg.name.clone()),
7317            "TargetGroupFullName" => Some(tg_full),
7318            "LoadBalancerArns" => Some(tg.load_balancer_arns.join(",")),
7319            _ => None,
7320        }
7321    }
7322
7323    /// Live-state GetAtt fallback for AWS::ElasticLoadBalancingV2::Listener.
7324    fn get_att_elbv2_listener(&self, physical_id: &str, attribute: &str) -> Option<String> {
7325        let mut accounts = self.elbv2_state.write();
7326        let state = accounts.get_or_create(&self.account_id);
7327        let listener = state.listeners.get(physical_id)?;
7328        match attribute {
7329            "Arn" | "ListenerArn" => Some(listener.arn.clone()),
7330            _ => None,
7331        }
7332    }
7333
7334    /// Live-state GetAtt fallback for AWS::ElasticLoadBalancingV2::ListenerRule.
7335    fn get_att_elbv2_listener_rule(&self, physical_id: &str, attribute: &str) -> Option<String> {
7336        let mut accounts = self.elbv2_state.write();
7337        let state = accounts.get_or_create(&self.account_id);
7338        let rule = state.rules.get(physical_id)?;
7339        match attribute {
7340            "RuleArn" => Some(rule.arn.clone()),
7341            "IsDefault" => Some(rule.is_default.to_string()),
7342            _ => None,
7343        }
7344    }
7345
7346    /// Live-state GetAtt fallback for AWS::ElasticLoadBalancingV2::TrustStore.
7347    fn get_att_elbv2_trust_store(&self, physical_id: &str, attribute: &str) -> Option<String> {
7348        let mut accounts = self.elbv2_state.write();
7349        let state = accounts.get_or_create(&self.account_id);
7350        let ts = state.trust_stores.get(physical_id)?;
7351        match attribute {
7352            "TrustStoreArn" => Some(ts.arn.clone()),
7353            "Name" => Some(ts.name.clone()),
7354            "Status" => Some(ts.status.clone()),
7355            "NumberOfCaCertificates" => Some(ts.number_of_ca_certificates.to_string()),
7356            "TotalRevokedEntries" => Some(ts.total_revoked_entries.to_string()),
7357            _ => None,
7358        }
7359    }
7360
7361    // --- Organizations ---
7362
7363    fn create_organization(
7364        &self,
7365        resource: &ResourceDefinition,
7366    ) -> Result<ProvisionResult, String> {
7367        let props = &resource.properties;
7368        let feature_set = props
7369            .get("FeatureSet")
7370            .and_then(|v| v.as_str())
7371            .unwrap_or("ALL")
7372            .to_string();
7373
7374        let mut org = self.organizations_state.write();
7375        if org.is_some() {
7376            return Err("Organization already exists; only one per fakecloud process".to_string());
7377        }
7378        let mut state = OrganizationState::bootstrap(&self.account_id);
7379        state.feature_set = feature_set;
7380        let org_id = state.org_id.clone();
7381        let org_arn = state.org_arn.clone();
7382        let mgmt_arn = state.management_account_arn.clone();
7383        let root_id = state.root_id.clone();
7384        *org = Some(state);
7385
7386        Ok(ProvisionResult::new(org_id.clone())
7387            .with("Id", org_id)
7388            .with("Arn", org_arn)
7389            .with("ManagementAccountArn", mgmt_arn)
7390            .with("RootId", root_id))
7391    }
7392
7393    fn delete_organization(&self, _physical_id: &str) -> Result<(), String> {
7394        let mut org = self.organizations_state.write();
7395        *org = None;
7396        Ok(())
7397    }
7398
7399    fn create_organization_unit(
7400        &self,
7401        resource: &ResourceDefinition,
7402    ) -> Result<ProvisionResult, String> {
7403        let props = &resource.properties;
7404        let name = props
7405            .get("Name")
7406            .and_then(|v| v.as_str())
7407            .unwrap_or(&resource.logical_id)
7408            .to_string();
7409        let parent_id = props
7410            .get("ParentId")
7411            .and_then(|v| v.as_str())
7412            .ok_or_else(|| "ParentId is required".to_string())?
7413            .to_string();
7414
7415        let mut org_lock = self.organizations_state.write();
7416        let org = org_lock
7417            .as_mut()
7418            .ok_or_else(|| "Organization not yet created".to_string())?;
7419        // Accept root id, OU id, or `Ref`-resolved logical id (we map to root).
7420        let resolved_parent_id = if parent_id == org.root_id || org.ous.contains_key(&parent_id) {
7421            parent_id
7422        } else {
7423            return Err(format!("Parent {parent_id} does not exist"));
7424        };
7425        let id_suffix: String = Uuid::new_v4()
7426            .simple()
7427            .to_string()
7428            .chars()
7429            .take(8)
7430            .collect();
7431        let id = format!("ou-{}-{}", &org.root_id[2..], id_suffix);
7432        let arn = format!(
7433            "arn:aws:organizations::{}:ou/{}/{}",
7434            org.management_account_id, org.org_id, id
7435        );
7436        org.ous.insert(
7437            id.clone(),
7438            OrganizationalUnit {
7439                id: id.clone(),
7440                arn: arn.clone(),
7441                name: name.clone(),
7442                parent_id: resolved_parent_id,
7443            },
7444        );
7445        Ok(ProvisionResult::new(id.clone())
7446            .with("Id", id)
7447            .with("Arn", arn)
7448            .with("Name", name))
7449    }
7450
7451    fn delete_organization_unit(&self, physical_id: &str) -> Result<(), String> {
7452        let mut org_lock = self.organizations_state.write();
7453        if let Some(org) = org_lock.as_mut() {
7454            org.ous.remove(physical_id);
7455            org.attachments.remove(physical_id);
7456        }
7457        Ok(())
7458    }
7459
7460    /// Provision an `AWS::Organizations::Account`. Mints a new member
7461    /// account synchronously (via Organizations state), optionally moves
7462    /// it under the first ParentId when supplied, and persists tags.
7463    fn create_organization_account(
7464        &self,
7465        resource: &ResourceDefinition,
7466    ) -> Result<ProvisionResult, String> {
7467        let props = &resource.properties;
7468        let email = props
7469            .get("Email")
7470            .and_then(|v| v.as_str())
7471            .ok_or_else(|| "Email is required".to_string())?
7472            .to_string();
7473        let name = props
7474            .get("AccountName")
7475            .and_then(|v| v.as_str())
7476            .ok_or_else(|| "AccountName is required".to_string())?
7477            .to_string();
7478        let parent_ids: Vec<String> = props
7479            .get("ParentIds")
7480            .and_then(|v| v.as_array())
7481            .map(|arr| {
7482                arr.iter()
7483                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
7484                    .collect()
7485            })
7486            .unwrap_or_default();
7487        let tags: Vec<(String, String)> = props
7488            .get("Tags")
7489            .and_then(|v| v.as_array())
7490            .map(|arr| {
7491                arr.iter()
7492                    .filter_map(|t| {
7493                        let k = t.get("Key").and_then(|v| v.as_str())?;
7494                        let val = t.get("Value").and_then(|v| v.as_str()).unwrap_or("");
7495                        Some((k.to_string(), val.to_string()))
7496                    })
7497                    .collect()
7498            })
7499            .unwrap_or_default();
7500
7501        let mut org_lock = self.organizations_state.write();
7502        let org = org_lock
7503            .as_mut()
7504            .ok_or_else(|| "Organization not yet created".to_string())?;
7505        // CFN provisioning is its own asynchronous flow; we don't need
7506        // a second layer of poll-for-completion on top. Begin the
7507        // request and immediately drive it to SUCCEEDED so the rest of
7508        // this provisioner sees a fully enrolled account.
7509        let pending = org.begin_create_account(&email, &name, None);
7510        let status = org.complete_create_account(&pending.id).unwrap_or(pending);
7511        let account_id = status
7512            .account_id
7513            .clone()
7514            .ok_or_else(|| "create_account did not return an account id".to_string())?;
7515        let account_arn = org
7516            .accounts
7517            .get(&account_id)
7518            .map(|a| a.arn.clone())
7519            .unwrap_or_default();
7520        let joined_method = org
7521            .accounts
7522            .get(&account_id)
7523            .map(|a| a.joined_method.clone())
7524            .unwrap_or_else(|| "CREATED".to_string());
7525        let joined_timestamp = org
7526            .accounts
7527            .get(&account_id)
7528            .map(|a| a.joined_timestamp.to_rfc3339())
7529            .unwrap_or_default();
7530        let acct_status = org
7531            .accounts
7532            .get(&account_id)
7533            .map(|a| a.status.clone())
7534            .unwrap_or_else(|| "ACTIVE".to_string());
7535
7536        if let Some(parent) = parent_ids.first() {
7537            let source = org
7538                .accounts
7539                .get(&account_id)
7540                .map(|a| a.parent_id.clone())
7541                .unwrap_or_else(|| org.root_id.clone());
7542            if parent != &source {
7543                org.move_account(&account_id, &source, parent)
7544                    .map_err(|e| format!("Failed to move account to parent {parent}: {e:?}"))?;
7545            }
7546        }
7547
7548        if !tags.is_empty() {
7549            org.set_resource_tags(&account_id, &tags);
7550        }
7551
7552        Ok(ProvisionResult::new(account_id.clone())
7553            .with("AccountId", account_id)
7554            .with("AccountName", name)
7555            .with("Email", email)
7556            .with("Arn", account_arn)
7557            .with("JoinedMethod", joined_method)
7558            .with("JoinedTimestamp", joined_timestamp)
7559            .with("Status", acct_status))
7560    }
7561
7562    /// Close the member account on stack delete. Real AWS leaves the
7563    /// account in `SUSPENDED` for 90 days; we just flip it via
7564    /// `close_account` so subsequent reads see it as suspended.
7565    fn delete_organization_account(&self, physical_id: &str) -> Result<(), String> {
7566        let mut org_lock = self.organizations_state.write();
7567        if let Some(org) = org_lock.as_mut() {
7568            let _ = org.close_account(physical_id);
7569        }
7570        Ok(())
7571    }
7572
7573    fn create_organization_policy(
7574        &self,
7575        resource: &ResourceDefinition,
7576    ) -> Result<ProvisionResult, String> {
7577        let props = &resource.properties;
7578        let name = props
7579            .get("Name")
7580            .and_then(|v| v.as_str())
7581            .unwrap_or(&resource.logical_id)
7582            .to_string();
7583        let description = props
7584            .get("Description")
7585            .and_then(|v| v.as_str())
7586            .unwrap_or("")
7587            .to_string();
7588        let policy_type = props
7589            .get("Type")
7590            .and_then(|v| v.as_str())
7591            .unwrap_or(POLICY_TYPE_SCP)
7592            .to_string();
7593        let content = props
7594            .get("Content")
7595            .map(|v| {
7596                if v.is_string() {
7597                    v.as_str().unwrap_or("").to_string()
7598                } else {
7599                    serde_json::to_string(v).unwrap_or_default()
7600                }
7601            })
7602            .unwrap_or_default();
7603        let target_ids: Vec<String> = props
7604            .get("TargetIds")
7605            .and_then(|v| v.as_array())
7606            .map(|arr| {
7607                arr.iter()
7608                    .filter_map(|t| t.as_str().map(|s| s.to_string()))
7609                    .collect()
7610            })
7611            .unwrap_or_default();
7612
7613        let mut org_lock = self.organizations_state.write();
7614        let org = org_lock
7615            .as_mut()
7616            .ok_or_else(|| "Organization not yet created".to_string())?;
7617        let id_suffix: String = Uuid::new_v4()
7618            .simple()
7619            .to_string()
7620            .chars()
7621            .take(8)
7622            .collect();
7623        let id = format!("p-{}", id_suffix);
7624        let arn = format!(
7625            "arn:aws:organizations::{}:policy/{}/{}/{}",
7626            org.management_account_id,
7627            org.org_id,
7628            policy_type.to_lowercase(),
7629            id
7630        );
7631        org.policies.insert(
7632            id.clone(),
7633            OrgPolicy {
7634                id: id.clone(),
7635                arn: arn.clone(),
7636                name: name.clone(),
7637                description,
7638                policy_type,
7639                aws_managed: false,
7640                content,
7641            },
7642        );
7643        for target in target_ids {
7644            org.attachments
7645                .entry(target)
7646                .or_default()
7647                .insert(id.clone());
7648        }
7649        Ok(ProvisionResult::new(id.clone())
7650            .with("Id", id)
7651            .with("Arn", arn)
7652            .with("Name", name))
7653    }
7654
7655    fn delete_organization_policy(&self, physical_id: &str) -> Result<(), String> {
7656        let mut org_lock = self.organizations_state.write();
7657        if let Some(org) = org_lock.as_mut() {
7658            org.policies.remove(physical_id);
7659            for attachments in org.attachments.values_mut() {
7660                attachments.remove(physical_id);
7661            }
7662        }
7663        Ok(())
7664    }
7665
7666    fn create_organization_resource_policy(
7667        &self,
7668        resource: &ResourceDefinition,
7669    ) -> Result<ProvisionResult, String> {
7670        let props = &resource.properties;
7671        let content = props
7672            .get("Content")
7673            .map(|v| {
7674                if v.is_string() {
7675                    v.as_str().unwrap_or("").to_string()
7676                } else {
7677                    serde_json::to_string(v).unwrap_or_default()
7678                }
7679            })
7680            .ok_or_else(|| "Content is required".to_string())?;
7681
7682        let mut org_lock = self.organizations_state.write();
7683        let org = org_lock
7684            .as_mut()
7685            .ok_or_else(|| "Organization not yet created".to_string())?;
7686        org.resource_policy = Some(content);
7687        let arn = format!(
7688            "arn:aws:organizations::{}:resourcepolicy/{}/rp",
7689            org.management_account_id, org.org_id
7690        );
7691        Ok(ProvisionResult::new(arn.clone()).with("Arn", arn))
7692    }
7693
7694    fn delete_organization_resource_policy(&self, _physical_id: &str) -> Result<(), String> {
7695        let mut org_lock = self.organizations_state.write();
7696        if let Some(org) = org_lock.as_mut() {
7697            org.resource_policy = None;
7698        }
7699        Ok(())
7700    }
7701
7702    fn delete_log_group(&self, physical_id: &str) -> Result<(), String> {
7703        let mut logs_accounts = self.logs_state.write();
7704        let state = logs_accounts.default_mut();
7705        // physical_id is the ARN; find the log group name
7706        let name = state
7707            .log_groups
7708            .iter()
7709            .find(|(_, g)| g.arn == physical_id)
7710            .map(|(name, _)| name.clone());
7711        if let Some(name) = name {
7712            state.log_groups.remove(&name);
7713        }
7714        Ok(())
7715    }
7716
7717    fn create_log_stream(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
7718        let props = &resource.properties;
7719        let log_group_name = props
7720            .get("LogGroupName")
7721            .and_then(|v| v.as_str())
7722            .map(parse_log_group_name)
7723            .ok_or_else(|| "LogGroupName is required".to_string())?;
7724        let log_stream_name = props
7725            .get("LogStreamName")
7726            .and_then(|v| v.as_str())
7727            .unwrap_or(&resource.logical_id)
7728            .to_string();
7729
7730        let mut logs_accounts = self.logs_state.write();
7731        let state = logs_accounts.get_or_create(&self.account_id);
7732        let group = state
7733            .log_groups
7734            .get_mut(&log_group_name)
7735            .ok_or_else(|| format!("Log group {log_group_name} does not exist"))?;
7736        let arn = format!(
7737            "arn:aws:logs:{}:{}:log-group:{}:log-stream:{}",
7738            self.region, self.account_id, log_group_name, log_stream_name
7739        );
7740        if group.log_streams.contains_key(&log_stream_name) {
7741            return Err(format!(
7742                "Log stream {log_stream_name} already exists in {log_group_name}"
7743            ));
7744        }
7745        group.log_streams.insert(
7746            log_stream_name.clone(),
7747            LogStream {
7748                name: log_stream_name.clone(),
7749                arn,
7750                creation_time: Utc::now().timestamp_millis(),
7751                first_event_timestamp: None,
7752                last_event_timestamp: None,
7753                last_ingestion_time: None,
7754                upload_sequence_token: String::new(),
7755                events: Vec::new(),
7756            },
7757        );
7758
7759        // Encode group + stream into the physical id so deletion can target both.
7760        let physical_id = format!("{log_group_name}|{log_stream_name}");
7761        Ok(ProvisionResult::new(physical_id))
7762    }
7763
7764    fn delete_log_stream(&self, physical_id: &str) -> Result<(), String> {
7765        let mut logs_accounts = self.logs_state.write();
7766        let state = logs_accounts.get_or_create(&self.account_id);
7767        if let Some((group_name, stream_name)) = physical_id.split_once('|') {
7768            if let Some(group) = state.log_groups.get_mut(group_name) {
7769                group.log_streams.remove(stream_name);
7770            }
7771        }
7772        Ok(())
7773    }
7774
7775    fn create_metric_filter(
7776        &self,
7777        resource: &ResourceDefinition,
7778    ) -> Result<ProvisionResult, String> {
7779        let props = &resource.properties;
7780        let log_group_name = props
7781            .get("LogGroupName")
7782            .and_then(|v| v.as_str())
7783            .map(parse_log_group_name)
7784            .ok_or_else(|| "LogGroupName is required".to_string())?;
7785        let filter_name = props
7786            .get("FilterName")
7787            .and_then(|v| v.as_str())
7788            .unwrap_or(&resource.logical_id)
7789            .to_string();
7790        let filter_pattern = props
7791            .get("FilterPattern")
7792            .and_then(|v| v.as_str())
7793            .unwrap_or("")
7794            .to_string();
7795
7796        let mut transformations: Vec<MetricTransformation> = Vec::new();
7797        if let Some(arr) = props
7798            .get("MetricTransformations")
7799            .and_then(|v| v.as_array())
7800        {
7801            for t in arr {
7802                let metric_name = t
7803                    .get("MetricName")
7804                    .and_then(|v| v.as_str())
7805                    .unwrap_or("")
7806                    .to_string();
7807                let metric_namespace = t
7808                    .get("MetricNamespace")
7809                    .and_then(|v| v.as_str())
7810                    .unwrap_or("")
7811                    .to_string();
7812                let metric_value = t
7813                    .get("MetricValue")
7814                    .and_then(|v| v.as_str())
7815                    .unwrap_or("1")
7816                    .to_string();
7817                let default_value = t.get("DefaultValue").and_then(|v| v.as_f64());
7818                transformations.push(MetricTransformation {
7819                    metric_name,
7820                    metric_namespace,
7821                    metric_value,
7822                    default_value,
7823                });
7824            }
7825        }
7826
7827        let mut logs_accounts = self.logs_state.write();
7828        let state = logs_accounts.get_or_create(&self.account_id);
7829        if !state.log_groups.contains_key(&log_group_name) {
7830            return Err(format!("Log group {log_group_name} does not exist"));
7831        }
7832        state
7833            .metric_filters
7834            .retain(|f| !(f.log_group_name == log_group_name && f.filter_name == filter_name));
7835        state.metric_filters.push(MetricFilter {
7836            filter_name: filter_name.clone(),
7837            filter_pattern,
7838            log_group_name: log_group_name.clone(),
7839            metric_transformations: transformations,
7840            creation_time: Utc::now().timestamp_millis(),
7841        });
7842
7843        Ok(ProvisionResult::new(format!(
7844            "{log_group_name}|{filter_name}"
7845        )))
7846    }
7847
7848    fn delete_metric_filter(&self, physical_id: &str) -> Result<(), String> {
7849        let mut logs_accounts = self.logs_state.write();
7850        let state = logs_accounts.get_or_create(&self.account_id);
7851        if let Some((group_name, filter_name)) = physical_id.split_once('|') {
7852            state
7853                .metric_filters
7854                .retain(|f| !(f.log_group_name == group_name && f.filter_name == filter_name));
7855        }
7856        Ok(())
7857    }
7858
7859    fn create_subscription_filter(
7860        &self,
7861        resource: &ResourceDefinition,
7862    ) -> Result<ProvisionResult, String> {
7863        let props = &resource.properties;
7864        let log_group_name = props
7865            .get("LogGroupName")
7866            .and_then(|v| v.as_str())
7867            .map(parse_log_group_name)
7868            .ok_or_else(|| "LogGroupName is required".to_string())?;
7869        let filter_name = props
7870            .get("FilterName")
7871            .and_then(|v| v.as_str())
7872            .unwrap_or(&resource.logical_id)
7873            .to_string();
7874        let filter_pattern = props
7875            .get("FilterPattern")
7876            .and_then(|v| v.as_str())
7877            .unwrap_or("")
7878            .to_string();
7879        let destination_arn = props
7880            .get("DestinationArn")
7881            .and_then(|v| v.as_str())
7882            .ok_or_else(|| "DestinationArn is required".to_string())?
7883            .to_string();
7884        let role_arn = props
7885            .get("RoleArn")
7886            .and_then(|v| v.as_str())
7887            .map(|s| s.to_string());
7888        let distribution = props
7889            .get("Distribution")
7890            .and_then(|v| v.as_str())
7891            .unwrap_or("ByLogStream")
7892            .to_string();
7893
7894        let mut logs_accounts = self.logs_state.write();
7895        let state = logs_accounts.get_or_create(&self.account_id);
7896        let group = state
7897            .log_groups
7898            .get_mut(&log_group_name)
7899            .ok_or_else(|| format!("Log group {log_group_name} does not exist"))?;
7900        group
7901            .subscription_filters
7902            .retain(|f| f.filter_name != filter_name);
7903        group.subscription_filters.push(SubscriptionFilter {
7904            filter_name: filter_name.clone(),
7905            log_group_name: log_group_name.clone(),
7906            filter_pattern,
7907            destination_arn,
7908            role_arn,
7909            distribution,
7910            creation_time: Utc::now().timestamp_millis(),
7911        });
7912
7913        Ok(ProvisionResult::new(format!(
7914            "{log_group_name}|{filter_name}"
7915        )))
7916    }
7917
7918    fn delete_subscription_filter(&self, physical_id: &str) -> Result<(), String> {
7919        let mut logs_accounts = self.logs_state.write();
7920        let state = logs_accounts.get_or_create(&self.account_id);
7921        if let Some((group_name, filter_name)) = physical_id.split_once('|') {
7922            if let Some(group) = state.log_groups.get_mut(group_name) {
7923                group
7924                    .subscription_filters
7925                    .retain(|f| f.filter_name != filter_name);
7926            }
7927        }
7928        Ok(())
7929    }
7930
7931    // --- Logs: Destination / ResourcePolicy / QueryDefinition / Delivery* ---
7932
7933    fn create_logs_destination(
7934        &self,
7935        resource: &ResourceDefinition,
7936    ) -> Result<ProvisionResult, String> {
7937        let props = &resource.properties;
7938        let destination_name = props
7939            .get("DestinationName")
7940            .and_then(|v| v.as_str())
7941            .ok_or("DestinationName is required")?
7942            .to_string();
7943        let target_arn = props
7944            .get("TargetArn")
7945            .and_then(|v| v.as_str())
7946            .ok_or("TargetArn is required")?
7947            .to_string();
7948        let role_arn = props
7949            .get("RoleArn")
7950            .and_then(|v| v.as_str())
7951            .ok_or("RoleArn is required")?
7952            .to_string();
7953        let access_policy = props
7954            .get("DestinationPolicy")
7955            .and_then(|v| v.as_str())
7956            .map(String::from);
7957
7958        let arn = format!(
7959            "arn:aws:logs:{}:{}:destination:{destination_name}",
7960            self.region, self.account_id
7961        );
7962        let dest = Destination {
7963            destination_name: destination_name.clone(),
7964            target_arn,
7965            role_arn,
7966            arn: arn.clone(),
7967            access_policy,
7968            creation_time: Utc::now().timestamp_millis(),
7969            tags: BTreeMap::new(),
7970        };
7971
7972        let mut logs_accounts = self.logs_state.write();
7973        let state = logs_accounts.get_or_create(&self.account_id);
7974        state.destinations.insert(destination_name.clone(), dest);
7975
7976        Ok(ProvisionResult::new(destination_name).with("Arn", arn))
7977    }
7978
7979    fn delete_logs_destination(&self, physical_id: &str) -> Result<(), String> {
7980        let mut logs_accounts = self.logs_state.write();
7981        let state = logs_accounts.get_or_create(&self.account_id);
7982        state.destinations.remove(physical_id);
7983        Ok(())
7984    }
7985
7986    fn create_logs_resource_policy(
7987        &self,
7988        resource: &ResourceDefinition,
7989    ) -> Result<ProvisionResult, String> {
7990        let props = &resource.properties;
7991        let policy_name = props
7992            .get("PolicyName")
7993            .and_then(|v| v.as_str())
7994            .ok_or("PolicyName is required")?
7995            .to_string();
7996        let policy_document = props
7997            .get("PolicyDocument")
7998            .map(|v| {
7999                if let Some(s) = v.as_str() {
8000                    s.to_string()
8001                } else {
8002                    serde_json::to_string(v).unwrap_or_default()
8003                }
8004            })
8005            .ok_or("PolicyDocument is required")?;
8006
8007        let policy = ResourcePolicy {
8008            policy_name: policy_name.clone(),
8009            policy_document,
8010            last_updated_time: Utc::now().timestamp_millis(),
8011        };
8012
8013        let mut logs_accounts = self.logs_state.write();
8014        let state = logs_accounts.get_or_create(&self.account_id);
8015        state.resource_policies.insert(policy_name.clone(), policy);
8016
8017        Ok(ProvisionResult::new(policy_name))
8018    }
8019
8020    fn delete_logs_resource_policy(&self, physical_id: &str) -> Result<(), String> {
8021        let mut logs_accounts = self.logs_state.write();
8022        let state = logs_accounts.get_or_create(&self.account_id);
8023        state.resource_policies.remove(physical_id);
8024        Ok(())
8025    }
8026
8027    fn create_logs_query_definition(
8028        &self,
8029        resource: &ResourceDefinition,
8030    ) -> Result<ProvisionResult, String> {
8031        let props = &resource.properties;
8032        let name = props
8033            .get("Name")
8034            .and_then(|v| v.as_str())
8035            .ok_or("Name is required")?
8036            .to_string();
8037        let query_string = props
8038            .get("QueryString")
8039            .and_then(|v| v.as_str())
8040            .ok_or("QueryString is required")?
8041            .to_string();
8042        let log_group_names: Vec<String> = props
8043            .get("LogGroupNames")
8044            .and_then(|v| v.as_array())
8045            .map(|arr| {
8046                arr.iter()
8047                    .filter_map(|v| v.as_str().map(String::from))
8048                    .collect()
8049            })
8050            .unwrap_or_default();
8051
8052        let id = Uuid::new_v4().to_string();
8053        let qd = QueryDefinition {
8054            query_definition_id: id.clone(),
8055            name,
8056            query_string,
8057            log_group_names,
8058            last_modified: Utc::now().timestamp_millis(),
8059        };
8060
8061        let mut logs_accounts = self.logs_state.write();
8062        let state = logs_accounts.get_or_create(&self.account_id);
8063        state.query_definitions.insert(id.clone(), qd);
8064
8065        Ok(ProvisionResult::new(id.clone()).with("QueryDefinitionId", id))
8066    }
8067
8068    fn delete_logs_query_definition(&self, physical_id: &str) -> Result<(), String> {
8069        let mut logs_accounts = self.logs_state.write();
8070        let state = logs_accounts.get_or_create(&self.account_id);
8071        state.query_definitions.remove(physical_id);
8072        Ok(())
8073    }
8074
8075    fn create_logs_delivery_destination(
8076        &self,
8077        resource: &ResourceDefinition,
8078    ) -> Result<ProvisionResult, String> {
8079        let props = &resource.properties;
8080        let name = props
8081            .get("Name")
8082            .and_then(|v| v.as_str())
8083            .ok_or("Name is required")?
8084            .to_string();
8085        let output_format = props
8086            .get("OutputFormat")
8087            .and_then(|v| v.as_str())
8088            .map(String::from);
8089        let mut configuration: BTreeMap<String, String> = BTreeMap::new();
8090        if let Some(arn) = props.get("DestinationResourceArn").and_then(|v| v.as_str()) {
8091            configuration.insert("destinationResourceArn".to_string(), arn.to_string());
8092        }
8093        if let Some(cfg) = props
8094            .get("DeliveryDestinationConfiguration")
8095            .and_then(|v| v.as_object())
8096        {
8097            for (k, v) in cfg {
8098                if let Some(s) = v.as_str() {
8099                    configuration.insert(k.clone(), s.to_string());
8100                }
8101            }
8102        }
8103        let policy = props.get("DeliveryDestinationPolicy").map(|v| {
8104            if let Some(s) = v.as_str() {
8105                s.to_string()
8106            } else {
8107                serde_json::to_string(v).unwrap_or_default()
8108            }
8109        });
8110
8111        let arn = format!(
8112            "arn:aws:logs:{}:{}:delivery-destination:{name}",
8113            self.region, self.account_id
8114        );
8115        let dd = DeliveryDestination {
8116            name: name.clone(),
8117            arn: arn.clone(),
8118            output_format,
8119            delivery_destination_configuration: configuration,
8120            tags: BTreeMap::new(),
8121            delivery_destination_policy: policy,
8122        };
8123
8124        let mut logs_accounts = self.logs_state.write();
8125        let state = logs_accounts.get_or_create(&self.account_id);
8126        state.delivery_destinations.insert(name.clone(), dd);
8127
8128        Ok(ProvisionResult::new(name).with("Arn", arn))
8129    }
8130
8131    fn delete_logs_delivery_destination(&self, physical_id: &str) -> Result<(), String> {
8132        let mut logs_accounts = self.logs_state.write();
8133        let state = logs_accounts.get_or_create(&self.account_id);
8134        state.delivery_destinations.remove(physical_id);
8135        Ok(())
8136    }
8137
8138    fn create_logs_delivery_source(
8139        &self,
8140        resource: &ResourceDefinition,
8141    ) -> Result<ProvisionResult, String> {
8142        let props = &resource.properties;
8143        let name = props
8144            .get("Name")
8145            .and_then(|v| v.as_str())
8146            .ok_or("Name is required")?
8147            .to_string();
8148        let resource_arns: Vec<String> = props
8149            .get("ResourceArn")
8150            .and_then(|v| v.as_str())
8151            .map(|s| vec![s.to_string()])
8152            .or_else(|| {
8153                props
8154                    .get("ResourceArns")
8155                    .and_then(|v| v.as_array())
8156                    .map(|arr| {
8157                        arr.iter()
8158                            .filter_map(|v| v.as_str().map(String::from))
8159                            .collect()
8160                    })
8161            })
8162            .unwrap_or_default();
8163        let log_type = props
8164            .get("LogType")
8165            .and_then(|v| v.as_str())
8166            .ok_or("LogType is required")?
8167            .to_string();
8168        let service = props
8169            .get("Service")
8170            .and_then(|v| v.as_str())
8171            .unwrap_or("")
8172            .to_string();
8173
8174        let arn = format!(
8175            "arn:aws:logs:{}:{}:delivery-source:{name}",
8176            self.region, self.account_id
8177        );
8178        let ds = DeliverySource {
8179            name: name.clone(),
8180            arn: arn.clone(),
8181            resource_arns,
8182            service,
8183            log_type,
8184            tags: BTreeMap::new(),
8185            created_at: chrono::Utc::now().timestamp_millis(),
8186        };
8187
8188        let mut logs_accounts = self.logs_state.write();
8189        let state = logs_accounts.get_or_create(&self.account_id);
8190        state.delivery_sources.insert(name.clone(), ds);
8191
8192        Ok(ProvisionResult::new(name).with("Arn", arn))
8193    }
8194
8195    fn delete_logs_delivery_source(&self, physical_id: &str) -> Result<(), String> {
8196        let mut logs_accounts = self.logs_state.write();
8197        let state = logs_accounts.get_or_create(&self.account_id);
8198        state.delivery_sources.remove(physical_id);
8199        Ok(())
8200    }
8201
8202    fn create_logs_delivery(
8203        &self,
8204        resource: &ResourceDefinition,
8205    ) -> Result<ProvisionResult, String> {
8206        let props = &resource.properties;
8207        let delivery_source_name = props
8208            .get("DeliverySourceName")
8209            .and_then(|v| v.as_str())
8210            .ok_or("DeliverySourceName is required")?
8211            .to_string();
8212        let delivery_destination_arn = props
8213            .get("DeliveryDestinationArn")
8214            .and_then(|v| v.as_str())
8215            .ok_or("DeliveryDestinationArn is required")?
8216            .to_string();
8217        // Infer destination type from the destination ARN service segment.
8218        let delivery_destination_type = if delivery_destination_arn.contains(":s3:") {
8219            "S3".to_string()
8220        } else if delivery_destination_arn.contains(":firehose:") {
8221            "FH".to_string()
8222        } else {
8223            "CWL".to_string()
8224        };
8225
8226        let id = Uuid::new_v4().simple().to_string();
8227        let arn = format!(
8228            "arn:aws:logs:{}:{}:delivery:{id}",
8229            self.region, self.account_id
8230        );
8231        let delivery = Delivery {
8232            id: id.clone(),
8233            delivery_source_name,
8234            delivery_destination_arn,
8235            delivery_destination_type,
8236            arn: arn.clone(),
8237            tags: BTreeMap::new(),
8238            field_delimiter: None,
8239            record_fields: Vec::new(),
8240            s3_delivery_configuration: None,
8241            created_at: chrono::Utc::now().timestamp_millis(),
8242        };
8243
8244        let mut logs_accounts = self.logs_state.write();
8245        let state = logs_accounts.get_or_create(&self.account_id);
8246        state.deliveries.insert(id.clone(), delivery);
8247
8248        Ok(ProvisionResult::new(id.clone())
8249            .with("DeliveryId", id)
8250            .with("Arn", arn))
8251    }
8252
8253    fn delete_logs_delivery(&self, physical_id: &str) -> Result<(), String> {
8254        let mut logs_accounts = self.logs_state.write();
8255        let state = logs_accounts.get_or_create(&self.account_id);
8256        state.deliveries.remove(physical_id);
8257        Ok(())
8258    }
8259
8260    // --- Custom Resources ---
8261
8262    /// Invoke a Lambda function synchronously via the delivery bus.
8263    fn invoke_lambda_sync(&self, function_arn: &str, payload: &str) -> Result<(), String> {
8264        let delivery = self.delivery.clone();
8265        let function_arn = function_arn.to_string();
8266        let payload = payload.to_string();
8267        std::thread::scope(|s| {
8268            s.spawn(|| {
8269                let rt = tokio::runtime::Builder::new_current_thread()
8270                    .enable_all()
8271                    .build()
8272                    .map_err(|e| format!("Failed to create runtime: {e}"))?;
8273                rt.block_on(async {
8274                    match delivery.invoke_lambda(&function_arn, &payload).await {
8275                        Some(Ok(_)) => {
8276                            tracing::info!(
8277                                "Custom resource Lambda {} invoked successfully",
8278                                function_arn
8279                            );
8280                            Ok(())
8281                        }
8282                        Some(Err(e)) => {
8283                            tracing::warn!(
8284                                "Custom resource Lambda {} invocation failed: {e}",
8285                                function_arn
8286                            );
8287                            Err(format!("Lambda invocation failed: {e}"))
8288                        }
8289                        None => {
8290                            tracing::warn!(
8291                                "No Lambda delivery configured; skipping custom resource invocation for {}",
8292                                function_arn
8293                            );
8294                            Ok(())
8295                        }
8296                    }
8297                })
8298            })
8299            .join()
8300            .map_err(|_| "Lambda invocation thread panicked".to_string())?
8301        })
8302    }
8303
8304    fn create_custom_resource(&self, resource: &ResourceDefinition) -> Result<String, String> {
8305        let props = &resource.properties;
8306        let service_token = props
8307            .get("ServiceToken")
8308            .and_then(|v| v.as_str())
8309            .ok_or("Custom resource requires ServiceToken property")?;
8310
8311        let request_id = Uuid::new_v4().to_string();
8312
8313        // Build the CloudFormation custom resource event
8314        let event = serde_json::json!({
8315            "RequestType": "Create",
8316            "ServiceToken": service_token,
8317            "StackId": self.stack_id,
8318            "RequestId": request_id,
8319            "ResourceType": resource.resource_type,
8320            "LogicalResourceId": resource.logical_id,
8321            "ResourceProperties": props,
8322        });
8323
8324        let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
8325        self.invoke_lambda_sync(service_token, &payload)?;
8326
8327        // Physical resource ID: use a generated ID (the Lambda could return one,
8328        // but for simplicity we generate one here).
8329        let physical_id = format!("{}-{}", resource.logical_id, &request_id[..8]);
8330        Ok(physical_id)
8331    }
8332
8333    fn delete_custom_resource(&self, resource: &StackResource) -> Result<(), String> {
8334        let service_token = match &resource.service_token {
8335            Some(token) => token.clone(),
8336            None => {
8337                // No ServiceToken stored — nothing to invoke
8338                return Ok(());
8339            }
8340        };
8341
8342        let request_id = Uuid::new_v4().to_string();
8343
8344        let event = serde_json::json!({
8345            "RequestType": "Delete",
8346            "ServiceToken": service_token,
8347            "StackId": self.stack_id,
8348            "RequestId": request_id,
8349            "ResourceType": resource.resource_type,
8350            "LogicalResourceId": resource.logical_id,
8351            "PhysicalResourceId": resource.physical_id,
8352        });
8353
8354        let payload = serde_json::to_string(&event).map_err(|e| e.to_string())?;
8355
8356        // Best-effort: don't fail stack deletion if Lambda invocation fails
8357        if let Err(e) = self.invoke_lambda_sync(&service_token, &payload) {
8358            tracing::warn!(
8359                "Custom resource delete Lambda invocation failed for {}: {e}",
8360                resource.logical_id
8361            );
8362        }
8363        Ok(())
8364    }
8365
8366    // --- Application Auto Scaling ---
8367
8368    fn create_application_autoscaling_scalable_target(
8369        &self,
8370        resource: &ResourceDefinition,
8371    ) -> Result<ProvisionResult, String> {
8372        let props = &resource.properties;
8373        let service_namespace = props
8374            .get("ServiceNamespace")
8375            .and_then(|v| v.as_str())
8376            .ok_or_else(|| "ServiceNamespace is required".to_string())?
8377            .to_string();
8378        let resource_id = props
8379            .get("ResourceId")
8380            .and_then(|v| v.as_str())
8381            .ok_or_else(|| "ResourceId is required".to_string())?
8382            .to_string();
8383        let scalable_dimension = props
8384            .get("ScalableDimension")
8385            .and_then(|v| v.as_str())
8386            .ok_or_else(|| "ScalableDimension is required".to_string())?
8387            .to_string();
8388        let min_capacity = props
8389            .get("MinCapacity")
8390            .and_then(|v| v.as_i64())
8391            .map(|n| n as i32)
8392            .ok_or_else(|| "MinCapacity is required".to_string())?;
8393        let max_capacity = props
8394            .get("MaxCapacity")
8395            .and_then(|v| v.as_i64())
8396            .map(|n| n as i32)
8397            .ok_or_else(|| "MaxCapacity is required".to_string())?;
8398        if min_capacity > max_capacity {
8399            return Err("MinCapacity must be <= MaxCapacity".to_string());
8400        }
8401        let role_arn = props
8402            .get("RoleARN")
8403            .and_then(|v| v.as_str())
8404            .map(|s| s.to_string());
8405        let suspended_state = props.get("SuspendedState").map(|v| AppasSuspendedState {
8406            dynamic_scaling_in_suspended: v
8407                .get("DynamicScalingInSuspended")
8408                .and_then(|x| x.as_bool()),
8409            dynamic_scaling_out_suspended: v
8410                .get("DynamicScalingOutSuspended")
8411                .and_then(|x| x.as_bool()),
8412            scheduled_scaling_suspended: v
8413                .get("ScheduledScalingSuspended")
8414                .and_then(|x| x.as_bool()),
8415        });
8416
8417        let arn = format!(
8418            "arn:aws:application-autoscaling:{}:{}:scalable-target/{}",
8419            self.region,
8420            self.account_id,
8421            &Uuid::new_v4().simple().to_string()[..10]
8422        );
8423        let role = role_arn.unwrap_or_else(|| {
8424            let suffix = match service_namespace.as_str() {
8425                "ecs" => "ECSService",
8426                "elasticmapreduce" => "EMRContainerService",
8427                "ec2" => "EC2SpotFleetRequest",
8428                "appstream" => "ApplicationAutoScaling_AppStreamFleet",
8429                "dynamodb" => "DynamoDBTable",
8430                "rds" => "RDSCluster",
8431                "sagemaker" => "SageMakerEndpoint",
8432                "lambda" => "LambdaConcurrency",
8433                "elasticache" => "ElastiCacheRG",
8434                "cassandra" => "CassandraTable",
8435                "kafka" => "KafkaCluster",
8436                _ => "ApplicationAutoScaling_Default",
8437            };
8438            format!(
8439                "arn:aws:iam::{}:role/aws-service-role/applicationautoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_{}",
8440                self.account_id, suffix
8441            )
8442        });
8443
8444        let mut state = self.app_autoscaling_state.write();
8445        let account = state.accounts.entry(self.account_id.clone()).or_default();
8446        let key = (
8447            service_namespace.clone(),
8448            resource_id.clone(),
8449            scalable_dimension.clone(),
8450        );
8451        let target = AppasScalableTarget {
8452            arn: arn.clone(),
8453            service_namespace: service_namespace.clone(),
8454            resource_id: resource_id.clone(),
8455            scalable_dimension: scalable_dimension.clone(),
8456            min_capacity,
8457            max_capacity,
8458            role_arn: role,
8459            creation_time: Utc::now(),
8460            suspended_state,
8461            predicted_capacity: None,
8462        };
8463        account.scalable_targets.insert(key, target);
8464
8465        Ok(ProvisionResult::new(resource_id.clone())
8466            .with("ScalableTargetARN", arn)
8467            .with("ServiceNamespace", service_namespace)
8468            .with("ScalableDimension", scalable_dimension))
8469    }
8470
8471    fn create_application_autoscaling_scaling_policy(
8472        &self,
8473        resource: &ResourceDefinition,
8474    ) -> Result<ProvisionResult, String> {
8475        let props = &resource.properties;
8476        let policy_name = props
8477            .get("PolicyName")
8478            .and_then(|v| v.as_str())
8479            .ok_or_else(|| "PolicyName is required".to_string())?
8480            .to_string();
8481        let service_namespace = props
8482            .get("ServiceNamespace")
8483            .and_then(|v| v.as_str())
8484            .ok_or_else(|| "ServiceNamespace is required".to_string())?
8485            .to_string();
8486        let resource_id = props
8487            .get("ResourceId")
8488            .and_then(|v| v.as_str())
8489            .ok_or_else(|| "ResourceId is required".to_string())?
8490            .to_string();
8491        let scalable_dimension = props
8492            .get("ScalableDimension")
8493            .and_then(|v| v.as_str())
8494            .ok_or_else(|| "ScalableDimension is required".to_string())?
8495            .to_string();
8496        let policy_type = props
8497            .get("PolicyType")
8498            .and_then(|v| v.as_str())
8499            .unwrap_or("StepScaling")
8500            .to_string();
8501        let step_cfg = props.get("StepScalingPolicyConfiguration").cloned();
8502        let tt_cfg = props
8503            .get("TargetTrackingScalingPolicyConfiguration")
8504            .cloned();
8505        let pred_cfg = props.get("PredictiveScalingPolicyConfiguration").cloned();
8506
8507        let target_key = (
8508            service_namespace.clone(),
8509            resource_id.clone(),
8510            scalable_dimension.clone(),
8511        );
8512        let policy_key = (
8513            service_namespace.clone(),
8514            resource_id.clone(),
8515            scalable_dimension.clone(),
8516            policy_name.clone(),
8517        );
8518
8519        let mut state = self.app_autoscaling_state.write();
8520        let account = state.accounts.entry(self.account_id.clone()).or_default();
8521        if !account.scalable_targets.contains_key(&target_key) {
8522            return Err(format!(
8523                "No scalable target registered for ServiceNamespace={} ResourceId={} ScalableDimension={}",
8524                service_namespace, resource_id, scalable_dimension
8525            ));
8526        }
8527        let arn = format!(
8528            "arn:aws:autoscaling:{}:{}:scalingPolicy:{}:resource/{}/{}:policyName/{}",
8529            self.region,
8530            self.account_id,
8531            Uuid::new_v4(),
8532            service_namespace,
8533            resource_id,
8534            policy_name
8535        );
8536        let policy = AppasScalingPolicy {
8537            arn: arn.clone(),
8538            policy_name: policy_name.clone(),
8539            service_namespace: service_namespace.clone(),
8540            resource_id: resource_id.clone(),
8541            scalable_dimension: scalable_dimension.clone(),
8542            policy_type: policy_type.clone(),
8543            creation_time: Utc::now(),
8544            step_scaling_policy_configuration: step_cfg,
8545            target_tracking_scaling_policy_configuration: tt_cfg,
8546            predictive_scaling_policy_configuration: pred_cfg,
8547            alarms: Vec::new(),
8548            last_applied_at: None,
8549        };
8550        account.scaling_policies.insert(policy_key, policy);
8551
8552        Ok(ProvisionResult::new(arn.clone())
8553            .with("PolicyName", policy_name)
8554            .with("ServiceNamespace", service_namespace)
8555            .with("ResourceId", resource_id)
8556            .with("ScalableDimension", scalable_dimension))
8557    }
8558
8559    fn delete_application_autoscaling_scalable_target(
8560        &self,
8561        physical_id: &str,
8562        attributes: &BTreeMap<String, String>,
8563    ) -> Result<(), String> {
8564        let namespace = attributes
8565            .get("ServiceNamespace")
8566            .cloned()
8567            .ok_or_else(|| "ServiceNamespace missing in attributes".to_string())?;
8568        let resource_id = physical_id.to_string();
8569        let dimension = attributes
8570            .get("ScalableDimension")
8571            .cloned()
8572            .ok_or_else(|| "ScalableDimension missing in attributes".to_string())?;
8573        let key = (namespace, resource_id.clone(), dimension);
8574
8575        let mut state = self.app_autoscaling_state.write();
8576        let account = state.accounts.entry(self.account_id.clone()).or_default();
8577        account.scalable_targets.remove(&key);
8578        account
8579            .scaling_policies
8580            .retain(|k, _| !(k.0 == key.0 && k.1 == key.1 && k.2 == key.2));
8581        account
8582            .scheduled_actions
8583            .retain(|k, _| !(k.0 == key.0 && k.1 == key.1 && k.2 == key.2));
8584        Ok(())
8585    }
8586
8587    fn delete_application_autoscaling_scaling_policy(
8588        &self,
8589        _physical_id: &str,
8590        attributes: &BTreeMap<String, String>,
8591    ) -> Result<(), String> {
8592        let policy_name = attributes
8593            .get("PolicyName")
8594            .cloned()
8595            .ok_or_else(|| "PolicyName missing in attributes".to_string())?;
8596        let namespace = attributes
8597            .get("ServiceNamespace")
8598            .cloned()
8599            .ok_or_else(|| "ServiceNamespace missing in attributes".to_string())?;
8600        let resource_id = attributes
8601            .get("ResourceId")
8602            .cloned()
8603            .ok_or_else(|| "ResourceId missing in attributes".to_string())?;
8604        let dimension = attributes
8605            .get("ScalableDimension")
8606            .cloned()
8607            .ok_or_else(|| "ScalableDimension missing in attributes".to_string())?;
8608        let key = (namespace, resource_id, dimension, policy_name);
8609
8610        let mut state = self.app_autoscaling_state.write();
8611        let account = state.accounts.entry(self.account_id.clone()).or_default();
8612        account.scaling_policies.remove(&key);
8613        Ok(())
8614    }
8615
8616    // --- Firehose ---
8617
8618    fn create_firehose_delivery_stream(
8619        &self,
8620        resource: &ResourceDefinition,
8621    ) -> Result<ProvisionResult, String> {
8622        let props = &resource.properties;
8623        let name = props
8624            .get("DeliveryStreamName")
8625            .and_then(|v| v.as_str())
8626            .unwrap_or(&resource.logical_id)
8627            .to_string();
8628
8629        let arn = format!(
8630            "arn:aws:firehose:{}:{}:deliverystream/{}",
8631            self.region, self.account_id, name
8632        );
8633        let stream_type = props
8634            .get("DeliveryStreamType")
8635            .and_then(|v| v.as_str())
8636            .unwrap_or("DirectPut")
8637            .to_string();
8638
8639        let has_s3 = props.get("S3DestinationConfiguration").is_some();
8640        let has_extended_s3 = props.get("ExtendedS3DestinationConfiguration").is_some();
8641        if has_s3 && has_extended_s3 {
8642            return Err("Only one of S3DestinationConfiguration or ExtendedS3DestinationConfiguration may be set".to_string());
8643        }
8644        let destination = Some(if let Some(s3) = props.get("S3DestinationConfiguration") {
8645            parse_firehose_s3_destination(s3)?
8646        } else if let Some(s3) = props.get("ExtendedS3DestinationConfiguration") {
8647            parse_firehose_s3_destination(s3)?
8648        } else {
8649            return Err("Delivery stream requires a destination configuration".to_string());
8650        });
8651
8652        let mut tags = BTreeMap::new();
8653        if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
8654            for tag in arr {
8655                if let (Some(k), Some(v)) = (
8656                    tag.get("Key").and_then(|v| v.as_str()),
8657                    tag.get("Value").and_then(|v| v.as_str()),
8658                ) {
8659                    tags.insert(k.to_string(), v.to_string());
8660                }
8661            }
8662        }
8663
8664        let stream = DeliveryStream {
8665            name: name.clone(),
8666            arn: arn.clone(),
8667            status: "ACTIVE".to_string(),
8668            stream_type: stream_type.clone(),
8669            created_at: Utc::now(),
8670            last_update: Utc::now(),
8671            version_id: "1".to_string(),
8672            destination,
8673            tags,
8674        };
8675
8676        let mut state = self.firehose_state.write();
8677        let account = state.get_or_create(&self.account_id, &self.region);
8678        account
8679            .streams_mut(&self.region)
8680            .insert(name.clone(), stream);
8681
8682        let mut attributes = BTreeMap::new();
8683        attributes.insert("Arn".to_string(), arn.clone());
8684        attributes.insert("DeliveryStreamName".to_string(), name.clone());
8685
8686        Ok(ProvisionResult {
8687            physical_id: name,
8688            attributes,
8689        })
8690    }
8691
8692    fn delete_firehose_delivery_stream(&self, physical_id: &str) -> Result<(), String> {
8693        let mut state = self.firehose_state.write();
8694        let account = state.get_or_create(&self.account_id, &self.region);
8695        account.streams_mut(&self.region).remove(physical_id);
8696        Ok(())
8697    }
8698
8699    // --- Cognito ---
8700
8701    fn create_cognito_user_pool(
8702        &self,
8703        resource: &ResourceDefinition,
8704    ) -> Result<ProvisionResult, String> {
8705        let props = &resource.properties;
8706        let pool_name = props
8707            .get("PoolName")
8708            .and_then(|v| v.as_str())
8709            .unwrap_or(&resource.logical_id)
8710            .to_string();
8711
8712        let pool_id = format!(
8713            "{}_{}",
8714            self.region,
8715            Uuid::new_v4()
8716                .simple()
8717                .to_string()
8718                .chars()
8719                .take(9)
8720                .collect::<String>()
8721        );
8722        let arn = format!(
8723            "arn:aws:cognito-idp:{}:{}:userpool/{}",
8724            self.region, self.account_id, pool_id
8725        );
8726        let now = Utc::now();
8727
8728        let password_policy = parse_cognito_password_policy(props.get("Policies"));
8729        let auto_verified = parse_cognito_string_array(props.get("AutoVerifiedAttributes"));
8730        let username_attributes = props
8731            .get("UsernameAttributes")
8732            .and_then(|v| v.as_array())
8733            .map(|_| parse_cognito_string_array(props.get("UsernameAttributes")));
8734        let alias_attributes = props
8735            .get("AliasAttributes")
8736            .and_then(|v| v.as_array())
8737            .map(|_| parse_cognito_string_array(props.get("AliasAttributes")));
8738        let mut schema_attributes = default_schema_attributes();
8739        if let Some(arr) = props.get("Schema").and_then(|v| v.as_array()) {
8740            for attr in arr {
8741                if let Some(parsed) = parse_cognito_schema_attribute(attr) {
8742                    if !schema_attributes.iter().any(|a| a.name == parsed.name) {
8743                        schema_attributes.push(parsed);
8744                    }
8745                }
8746            }
8747        }
8748        let mfa_configuration = props
8749            .get("MfaConfiguration")
8750            .and_then(|v| v.as_str())
8751            .unwrap_or("OFF")
8752            .to_string();
8753        let user_pool_tier = props
8754            .get("UserPoolTier")
8755            .and_then(|v| v.as_str())
8756            .unwrap_or("ESSENTIALS")
8757            .to_string();
8758        let deletion_protection = props
8759            .get("DeletionProtection")
8760            .and_then(|v| v.as_str())
8761            .map(|s| s.to_string());
8762        let user_pool_tags = parse_cognito_tags(props.get("UserPoolTags"));
8763        let email_configuration =
8764            parse_cognito_email_configuration(props.get("EmailConfiguration"));
8765        let sms_configuration = parse_cognito_sms_configuration(props.get("SmsConfiguration"));
8766        let admin_create_user_config =
8767            parse_cognito_admin_create_user_config(props.get("AdminCreateUserConfig"));
8768        let account_recovery_setting =
8769            parse_cognito_account_recovery(props.get("AccountRecoverySetting"));
8770
8771        // Generate the RSA-2048 keypair eagerly. The kid is derived
8772        // from a SHA-256 of the public SPKI DER so it stays stable
8773        // across snapshots and matches the JWKS document.
8774        let signing = fakecloud_cognito::jwt::generate_pool_signing_key();
8775        let signing_key_pem = signing.private_key_pem;
8776        let signing_kid = signing.kid;
8777        let pool = UserPool {
8778            id: pool_id.clone(),
8779            name: pool_name,
8780            arn: arn.clone(),
8781            status: "ACTIVE".to_string(),
8782            creation_date: now,
8783            last_modified_date: now,
8784            policies: PoolPolicies {
8785                password_policy,
8786                sign_in_policy: SignInPolicy {
8787                    allowed_first_auth_factors: vec!["PASSWORD".to_string()],
8788                },
8789            },
8790            auto_verified_attributes: auto_verified,
8791            username_attributes,
8792            alias_attributes,
8793            schema_attributes,
8794            lambda_config: None,
8795            mfa_configuration,
8796            email_configuration,
8797            sms_configuration,
8798            admin_create_user_config,
8799            user_pool_tags,
8800            account_recovery_setting,
8801            deletion_protection,
8802            estimated_number_of_users: 0,
8803            software_token_mfa_configuration: None,
8804            sms_mfa_configuration: None,
8805            user_pool_tier,
8806            verification_message_template: None,
8807            signing_key_pem: Some(signing_key_pem),
8808            signing_kid: Some(signing_kid),
8809            email_verification_message: None,
8810            email_verification_subject: None,
8811            sms_verification_message: None,
8812            sms_authentication_message: None,
8813            device_configuration: None,
8814            user_attribute_update_settings: None,
8815            user_pool_add_ons: None,
8816            username_configuration: None,
8817        };
8818
8819        let mut accounts = self.cognito_state.write();
8820        let state = accounts.get_or_create(&self.account_id);
8821        state.user_pools.insert(pool_id.clone(), pool);
8822
8823        let provider_name = format!("cognito-idp.{}.amazonaws.com/{}", self.region, pool_id);
8824        let provider_url = format!("https://{provider_name}");
8825
8826        Ok(ProvisionResult::new(pool_id.clone())
8827            .with("Arn", arn)
8828            .with("ProviderName", provider_name)
8829            .with("ProviderURL", provider_url)
8830            .with("UserPoolId", pool_id))
8831    }
8832
8833    fn delete_cognito_user_pool(&self, physical_id: &str) -> Result<(), String> {
8834        let mut accounts = self.cognito_state.write();
8835        let state = accounts.get_or_create(&self.account_id);
8836        state.user_pools.remove(physical_id);
8837        // Cascade: drop clients tied to this pool, plus per-pool side maps.
8838        state
8839            .user_pool_clients
8840            .retain(|_, c| c.user_pool_id != physical_id);
8841        state.users.remove(physical_id);
8842        state.groups.remove(physical_id);
8843        state.user_groups.remove(physical_id);
8844        state.identity_providers.remove(physical_id);
8845        state.resource_servers.remove(physical_id);
8846        state.import_jobs.remove(physical_id);
8847        state.domains.retain(|_, d| d.user_pool_id != physical_id);
8848        Ok(())
8849    }
8850
8851    fn create_cognito_user_pool_client(
8852        &self,
8853        resource: &ResourceDefinition,
8854    ) -> Result<ProvisionResult, String> {
8855        let props = &resource.properties;
8856        let pool_id = props
8857            .get("UserPoolId")
8858            .and_then(|v| v.as_str())
8859            .ok_or_else(|| "UserPoolId is required".to_string())?
8860            .to_string();
8861        let client_name = props
8862            .get("ClientName")
8863            .and_then(|v| v.as_str())
8864            .unwrap_or(&resource.logical_id)
8865            .to_string();
8866
8867        let mut accounts = self.cognito_state.write();
8868        let state = accounts.get_or_create(&self.account_id);
8869        if !state.user_pools.contains_key(&pool_id) {
8870            // Force CFN to retry once UserPool resource provisions.
8871            return Err(format!(
8872                "User pool {pool_id} does not exist yet — retry once it has been provisioned"
8873            ));
8874        }
8875
8876        let client_id: String = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple())
8877            .chars()
8878            .filter(|c| c.is_ascii_alphanumeric())
8879            .take(26)
8880            .collect::<String>()
8881            .to_lowercase();
8882        let generate_secret = props
8883            .get("GenerateSecret")
8884            .and_then(|v| v.as_bool())
8885            .unwrap_or(false);
8886        let client_secret = if generate_secret {
8887            use base64::Engine;
8888            let mut bytes = Vec::with_capacity(48);
8889            for _ in 0..3 {
8890                bytes.extend_from_slice(Uuid::new_v4().as_bytes());
8891            }
8892            Some(
8893                base64::engine::general_purpose::STANDARD
8894                    .encode(&bytes)
8895                    .chars()
8896                    .take(51)
8897                    .collect(),
8898            )
8899        } else {
8900            None
8901        };
8902
8903        let now = Utc::now();
8904        let client = UserPoolClient {
8905            client_id: client_id.clone(),
8906            client_name,
8907            user_pool_id: pool_id.clone(),
8908            client_secret: client_secret.clone(),
8909            explicit_auth_flows: parse_cognito_string_array(props.get("ExplicitAuthFlows")),
8910            token_validity_units: None,
8911            access_token_validity: props.get("AccessTokenValidity").and_then(|v| v.as_i64()),
8912            id_token_validity: props.get("IdTokenValidity").and_then(|v| v.as_i64()),
8913            refresh_token_validity: props.get("RefreshTokenValidity").and_then(|v| v.as_i64()),
8914            callback_urls: parse_cognito_string_array(props.get("CallbackURLs")),
8915            logout_urls: parse_cognito_string_array(props.get("LogoutURLs")),
8916            supported_identity_providers: parse_cognito_string_array(
8917                props.get("SupportedIdentityProviders"),
8918            ),
8919            allowed_o_auth_flows: parse_cognito_string_array(props.get("AllowedOAuthFlows")),
8920            allowed_o_auth_scopes: parse_cognito_string_array(props.get("AllowedOAuthScopes")),
8921            allowed_o_auth_flows_user_pool_client: props
8922                .get("AllowedOAuthFlowsUserPoolClient")
8923                .and_then(|v| v.as_bool())
8924                .unwrap_or(false),
8925            prevent_user_existence_errors: props
8926                .get("PreventUserExistenceErrors")
8927                .and_then(|v| v.as_str())
8928                .map(|s| s.to_string()),
8929            read_attributes: parse_cognito_string_array(props.get("ReadAttributes")),
8930            write_attributes: parse_cognito_string_array(props.get("WriteAttributes")),
8931            creation_date: now,
8932            last_modified_date: now,
8933            enable_token_revocation: props
8934                .get("EnableTokenRevocation")
8935                .and_then(|v| v.as_bool())
8936                .unwrap_or(true),
8937            auth_session_validity: props.get("AuthSessionValidity").and_then(|v| v.as_i64()),
8938            client_secrets: Vec::new(),
8939            refresh_token_rotation: None,
8940        };
8941
8942        state.user_pool_clients.insert(client_id.clone(), client);
8943
8944        let mut result = ProvisionResult::new(client_id.clone())
8945            .with("ClientId", client_id.clone())
8946            .with("Name", client_id);
8947        if let Some(secret) = client_secret {
8948            result = result.with("ClientSecret", secret);
8949        }
8950        Ok(result)
8951    }
8952
8953    fn delete_cognito_user_pool_client(&self, physical_id: &str) -> Result<(), String> {
8954        let mut accounts = self.cognito_state.write();
8955        let state = accounts.get_or_create(&self.account_id);
8956        state.user_pool_clients.remove(physical_id);
8957        Ok(())
8958    }
8959
8960    fn create_cognito_user_pool_domain(
8961        &self,
8962        resource: &ResourceDefinition,
8963    ) -> Result<ProvisionResult, String> {
8964        let props = &resource.properties;
8965        let domain = props
8966            .get("Domain")
8967            .and_then(|v| v.as_str())
8968            .ok_or_else(|| "Domain is required".to_string())?
8969            .to_string();
8970        let pool_id = props
8971            .get("UserPoolId")
8972            .and_then(|v| v.as_str())
8973            .ok_or_else(|| "UserPoolId is required".to_string())?
8974            .to_string();
8975        let custom_domain_config = props
8976            .get("CustomDomainConfig")
8977            .and_then(|v| v.as_object())
8978            .and_then(|m| {
8979                m.get("CertificateArn")
8980                    .and_then(|v| v.as_str())
8981                    .map(|s| CustomDomainConfig {
8982                        certificate_arn: s.to_string(),
8983                    })
8984            });
8985
8986        let mut accounts = self.cognito_state.write();
8987        let state = accounts.get_or_create(&self.account_id);
8988        if !state.user_pools.contains_key(&pool_id) {
8989            return Err(format!(
8990                "User pool {pool_id} does not exist yet — retry once it has been provisioned"
8991            ));
8992        }
8993        if state.domains.contains_key(&domain) {
8994            return Err(format!("Domain {domain} already exists"));
8995        }
8996        state.domains.insert(
8997            domain.clone(),
8998            UserPoolDomain {
8999                user_pool_id: pool_id,
9000                domain: domain.clone(),
9001                status: "ACTIVE".to_string(),
9002                custom_domain_config: custom_domain_config.clone(),
9003                creation_date: Utc::now(),
9004            },
9005        );
9006
9007        let cloudfront_distribution = if custom_domain_config.is_some() {
9008            format!("{domain}.cloudfront.net")
9009        } else {
9010            format!("{domain}.auth.{}.amazoncognito.com", self.region)
9011        };
9012
9013        Ok(ProvisionResult::new(domain.clone())
9014            .with("Domain", domain)
9015            .with("CloudFrontDistribution", cloudfront_distribution))
9016    }
9017
9018    fn delete_cognito_user_pool_domain(&self, physical_id: &str) -> Result<(), String> {
9019        let mut accounts = self.cognito_state.write();
9020        let state = accounts.get_or_create(&self.account_id);
9021        state.domains.remove(physical_id);
9022        Ok(())
9023    }
9024
9025    fn create_cognito_identity_pool(
9026        &self,
9027        resource: &ResourceDefinition,
9028    ) -> Result<ProvisionResult, String> {
9029        let props = &resource.properties;
9030        let identity_pool_name = props
9031            .get("IdentityPoolName")
9032            .and_then(|v| v.as_str())
9033            .unwrap_or(&resource.logical_id)
9034            .to_string();
9035        let allow_unauth = props
9036            .get("AllowUnauthenticatedIdentities")
9037            .and_then(|v| v.as_bool())
9038            .unwrap_or(false);
9039        let allow_classic = props
9040            .get("AllowClassicFlow")
9041            .and_then(|v| v.as_bool())
9042            .unwrap_or(false);
9043        let developer_provider_name = props
9044            .get("DeveloperProviderName")
9045            .and_then(|v| v.as_str())
9046            .map(|s| s.to_string());
9047        let cognito_identity_providers = props
9048            .get("CognitoIdentityProviders")
9049            .and_then(|v| v.as_array())
9050            .map(|arr| {
9051                arr.iter()
9052                    .filter_map(|p| {
9053                        let obj = p.as_object()?;
9054                        let provider_name = obj
9055                            .get("ProviderName")
9056                            .and_then(|v| v.as_str())?
9057                            .to_string();
9058                        let client_id = obj.get("ClientId").and_then(|v| v.as_str())?.to_string();
9059                        let server_side_token_check = obj
9060                            .get("ServerSideTokenCheck")
9061                            .and_then(|v| v.as_bool())
9062                            .unwrap_or(false);
9063                        Some(CognitoIdentityProvider {
9064                            provider_name,
9065                            client_id,
9066                            server_side_token_check,
9067                        })
9068                    })
9069                    .collect::<Vec<_>>()
9070            })
9071            .unwrap_or_default();
9072        let open_id_connect_provider_arns =
9073            parse_cognito_string_array(props.get("OpenIdConnectProviderARNs"));
9074        let saml_provider_arns = parse_cognito_string_array(props.get("SamlProviderARNs"));
9075        let supported_login_providers = props
9076            .get("SupportedLoginProviders")
9077            .and_then(|v| v.as_object())
9078            .map(|m| {
9079                m.iter()
9080                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
9081                    .collect()
9082            })
9083            .unwrap_or_default();
9084        let identity_pool_tags = parse_cognito_tags(props.get("IdentityPoolTags"));
9085        let cognito_streams = props.get("CognitoStreams").cloned();
9086        let push_sync = props.get("PushSync").cloned();
9087
9088        // Real Cognito identity pool ids look like `<region>:<uuid>`. Match
9089        // that shape so SDKs that parse it don't choke.
9090        let identity_pool_id = format!("{}:{}", self.region, Uuid::new_v4());
9091
9092        let pool = IdentityPool {
9093            identity_pool_id: identity_pool_id.clone(),
9094            identity_pool_name,
9095            allow_unauthenticated_identities: allow_unauth,
9096            allow_classic_flow: allow_classic,
9097            developer_provider_name,
9098            cognito_identity_providers,
9099            open_id_connect_provider_arns,
9100            saml_provider_arns,
9101            supported_login_providers,
9102            cognito_streams,
9103            push_sync,
9104            identity_pool_tags,
9105            creation_date: Utc::now(),
9106        };
9107
9108        let mut accounts = self.cognito_state.write();
9109        let state = accounts.get_or_create(&self.account_id);
9110        state.identity_pools.insert(identity_pool_id.clone(), pool);
9111
9112        Ok(ProvisionResult::new(identity_pool_id.clone()).with("Name", identity_pool_id))
9113    }
9114
9115    fn delete_cognito_identity_pool(&self, physical_id: &str) -> Result<(), String> {
9116        let mut accounts = self.cognito_state.write();
9117        let state = accounts.get_or_create(&self.account_id);
9118        state.identity_pools.remove(physical_id);
9119        // Cascade: drop role attachments tied to this pool.
9120        state
9121            .identity_pool_role_attachments
9122            .retain(|_, a| a.identity_pool_id != physical_id);
9123        Ok(())
9124    }
9125
9126    fn create_cognito_identity_pool_role_attachment(
9127        &self,
9128        resource: &ResourceDefinition,
9129    ) -> Result<ProvisionResult, String> {
9130        let props = &resource.properties;
9131        let identity_pool_id = props
9132            .get("IdentityPoolId")
9133            .and_then(|v| v.as_str())
9134            .ok_or_else(|| "IdentityPoolId is required".to_string())?
9135            .to_string();
9136
9137        let mut accounts = self.cognito_state.write();
9138        let state = accounts.get_or_create(&self.account_id);
9139        if !state.identity_pools.contains_key(&identity_pool_id) {
9140            return Err(format!(
9141                "Identity pool {identity_pool_id} does not exist yet — retry once it has been provisioned"
9142            ));
9143        }
9144
9145        let roles = props
9146            .get("Roles")
9147            .and_then(|v| v.as_object())
9148            .map(|m| {
9149                m.iter()
9150                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
9151                    .collect()
9152            })
9153            .unwrap_or_default();
9154        let role_mappings = props
9155            .get("RoleMappings")
9156            .and_then(|v| v.as_object())
9157            .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
9158            .unwrap_or_default();
9159
9160        let attachment_id = Uuid::new_v4().simple().to_string();
9161        let physical_id = format!("{identity_pool_id}:{attachment_id}");
9162        let attachment = IdentityPoolRoleAttachment {
9163            identity_pool_id: identity_pool_id.clone(),
9164            attachment_id,
9165            roles,
9166            role_mappings,
9167        };
9168        state
9169            .identity_pool_role_attachments
9170            .insert(physical_id.clone(), attachment);
9171
9172        Ok(ProvisionResult::new(physical_id))
9173    }
9174
9175    fn delete_cognito_identity_pool_role_attachment(
9176        &self,
9177        physical_id: &str,
9178    ) -> Result<(), String> {
9179        let mut accounts = self.cognito_state.write();
9180        let state = accounts.get_or_create(&self.account_id);
9181        state.identity_pool_role_attachments.remove(physical_id);
9182        Ok(())
9183    }
9184
9185    // --- RDS ---
9186
9187    fn create_rds_subnet_group(
9188        &self,
9189        resource: &ResourceDefinition,
9190    ) -> Result<ProvisionResult, String> {
9191        let props = &resource.properties;
9192        let name = props
9193            .get("DBSubnetGroupName")
9194            .and_then(|v| v.as_str())
9195            .unwrap_or(&resource.logical_id)
9196            .to_string();
9197        let description = props
9198            .get("DBSubnetGroupDescription")
9199            .and_then(|v| v.as_str())
9200            .unwrap_or("")
9201            .to_string();
9202        let subnet_ids: Vec<String> = props
9203            .get("SubnetIds")
9204            .and_then(|v| v.as_array())
9205            .map(|arr| {
9206                arr.iter()
9207                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
9208                    .collect()
9209            })
9210            .unwrap_or_default();
9211        let tags = parse_rds_tags(props.get("Tags"));
9212        let mut accounts = self.rds_state.write();
9213        let state = accounts.get_or_create(&self.account_id);
9214        let arn = state.db_subnet_group_arn(&name);
9215        let group = DbSubnetGroup {
9216            db_subnet_group_name: name.clone(),
9217            db_subnet_group_arn: arn.clone(),
9218            db_subnet_group_description: description,
9219            vpc_id: String::new(),
9220            subnet_ids,
9221            subnet_availability_zones: Vec::new(),
9222            tags,
9223        };
9224        state.subnet_groups.insert(name.clone(), group);
9225        Ok(ProvisionResult::new(name).with("Arn", arn))
9226    }
9227
9228    fn delete_rds_subnet_group(&self, physical_id: &str) -> Result<(), String> {
9229        let mut accounts = self.rds_state.write();
9230        let state = accounts.get_or_create(&self.account_id);
9231        state.subnet_groups.remove(physical_id);
9232        Ok(())
9233    }
9234
9235    fn create_rds_parameter_group(
9236        &self,
9237        resource: &ResourceDefinition,
9238    ) -> Result<ProvisionResult, String> {
9239        let props = &resource.properties;
9240        let name = props
9241            .get("DBParameterGroupName")
9242            .and_then(|v| v.as_str())
9243            .unwrap_or(&resource.logical_id)
9244            .to_string();
9245        let family = props
9246            .get("Family")
9247            .and_then(|v| v.as_str())
9248            .unwrap_or("postgres16")
9249            .to_string();
9250        let description = props
9251            .get("Description")
9252            .and_then(|v| v.as_str())
9253            .unwrap_or("")
9254            .to_string();
9255        let parameters: std::collections::BTreeMap<String, String> = props
9256            .get("Parameters")
9257            .and_then(|v| v.as_object())
9258            .map(|m| {
9259                m.iter()
9260                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
9261                    .collect()
9262            })
9263            .unwrap_or_default();
9264        let tags = parse_rds_tags(props.get("Tags"));
9265
9266        let mut accounts = self.rds_state.write();
9267        let state = accounts.get_or_create(&self.account_id);
9268        let arn = state.db_parameter_group_arn(&name);
9269        let group = DbParameterGroup {
9270            db_parameter_group_name: name.clone(),
9271            db_parameter_group_arn: arn.clone(),
9272            db_parameter_group_family: family,
9273            description,
9274            parameters,
9275            tags,
9276        };
9277        state.parameter_groups.insert(name.clone(), group);
9278        Ok(ProvisionResult::new(name).with("Arn", arn))
9279    }
9280
9281    fn delete_rds_parameter_group(&self, physical_id: &str) -> Result<(), String> {
9282        let mut accounts = self.rds_state.write();
9283        let state = accounts.get_or_create(&self.account_id);
9284        state.parameter_groups.remove(physical_id);
9285        Ok(())
9286    }
9287
9288    fn create_rds_cluster_parameter_group(
9289        &self,
9290        resource: &ResourceDefinition,
9291    ) -> Result<ProvisionResult, String> {
9292        let props = &resource.properties;
9293        let name = props
9294            .get("DBClusterParameterGroupName")
9295            .or_else(|| props.get("Name"))
9296            .and_then(|v| v.as_str())
9297            .unwrap_or(&resource.logical_id)
9298            .to_string();
9299        let family = props
9300            .get("Family")
9301            .and_then(|v| v.as_str())
9302            .unwrap_or("aurora-postgresql15")
9303            .to_string();
9304        let description = props
9305            .get("Description")
9306            .and_then(|v| v.as_str())
9307            .unwrap_or("")
9308            .to_string();
9309        let arn = format!(
9310            "arn:aws:rds:{}:{}:cluster-pg:{}",
9311            self.region, self.account_id, name
9312        );
9313        let entry = serde_json::json!({
9314            "DBClusterParameterGroupName": name,
9315            "DBClusterParameterGroupArn": arn,
9316            "DBParameterGroupFamily": family,
9317            "Description": description,
9318        });
9319        let mut accounts = self.rds_state.write();
9320        let state = accounts.get_or_create(&self.account_id);
9321        rds_extras_mut(state, "cluster_param_groups").insert(name.clone(), entry);
9322        Ok(ProvisionResult::new(name).with("Arn", arn))
9323    }
9324
9325    fn delete_rds_cluster_parameter_group(&self, physical_id: &str) -> Result<(), String> {
9326        let mut accounts = self.rds_state.write();
9327        let state = accounts.get_or_create(&self.account_id);
9328        if let Some(m) = state.extras.get_mut("cluster_param_groups") {
9329            m.remove(physical_id);
9330        }
9331        Ok(())
9332    }
9333
9334    fn create_rds_option_group(
9335        &self,
9336        resource: &ResourceDefinition,
9337    ) -> Result<ProvisionResult, String> {
9338        let props = &resource.properties;
9339        let name = props
9340            .get("OptionGroupName")
9341            .and_then(|v| v.as_str())
9342            .unwrap_or(&resource.logical_id)
9343            .to_string();
9344        let engine_name = props
9345            .get("EngineName")
9346            .and_then(|v| v.as_str())
9347            .unwrap_or("mysql")
9348            .to_string();
9349        let major_engine_version = props
9350            .get("MajorEngineVersion")
9351            .and_then(|v| v.as_str())
9352            .unwrap_or("8.0")
9353            .to_string();
9354        let description = props
9355            .get("OptionGroupDescription")
9356            .and_then(|v| v.as_str())
9357            .unwrap_or("")
9358            .to_string();
9359        let arn = format!(
9360            "arn:aws:rds:{}:{}:og:{}",
9361            self.region, self.account_id, name
9362        );
9363        let entry = serde_json::json!({
9364            "OptionGroupName": name,
9365            "OptionGroupArn": arn,
9366            "EngineName": engine_name,
9367            "MajorEngineVersion": major_engine_version,
9368            "OptionGroupDescription": description,
9369        });
9370        let mut accounts = self.rds_state.write();
9371        let state = accounts.get_or_create(&self.account_id);
9372        rds_extras_mut(state, "option_groups").insert(name.clone(), entry);
9373        Ok(ProvisionResult::new(name).with("Arn", arn))
9374    }
9375
9376    fn delete_rds_option_group(&self, physical_id: &str) -> Result<(), String> {
9377        let mut accounts = self.rds_state.write();
9378        let state = accounts.get_or_create(&self.account_id);
9379        if let Some(m) = state.extras.get_mut("option_groups") {
9380            m.remove(physical_id);
9381        }
9382        Ok(())
9383    }
9384
9385    fn create_rds_event_subscription(
9386        &self,
9387        resource: &ResourceDefinition,
9388    ) -> Result<ProvisionResult, String> {
9389        let props = &resource.properties;
9390        let name = props
9391            .get("SubscriptionName")
9392            .and_then(|v| v.as_str())
9393            .unwrap_or(&resource.logical_id)
9394            .to_string();
9395        let sns_topic_arn = props
9396            .get("SnsTopicArn")
9397            .and_then(|v| v.as_str())
9398            .unwrap_or("")
9399            .to_string();
9400        let entry = serde_json::json!({
9401            "CustSubscriptionId": name,
9402            "SnsTopicArn": sns_topic_arn,
9403            "Status": "active",
9404            "Enabled": props.get("Enabled").and_then(|v| v.as_bool()).unwrap_or(true),
9405        });
9406        let mut accounts = self.rds_state.write();
9407        let state = accounts.get_or_create(&self.account_id);
9408        rds_extras_mut(state, "event_subscriptions").insert(name.clone(), entry);
9409        Ok(ProvisionResult::new(name))
9410    }
9411
9412    fn delete_rds_event_subscription(&self, physical_id: &str) -> Result<(), String> {
9413        let mut accounts = self.rds_state.write();
9414        let state = accounts.get_or_create(&self.account_id);
9415        if let Some(m) = state.extras.get_mut("event_subscriptions") {
9416            m.remove(physical_id);
9417        }
9418        Ok(())
9419    }
9420
9421    fn create_rds_security_group(
9422        &self,
9423        resource: &ResourceDefinition,
9424    ) -> Result<ProvisionResult, String> {
9425        let props = &resource.properties;
9426        let name = props
9427            .get("DBSecurityGroupName")
9428            .and_then(|v| v.as_str())
9429            .unwrap_or(&resource.logical_id)
9430            .to_string();
9431        let description = props
9432            .get("GroupDescription")
9433            .and_then(|v| v.as_str())
9434            .unwrap_or("")
9435            .to_string();
9436        let entry = serde_json::json!({
9437            "DBSecurityGroupName": name,
9438            "DBSecurityGroupDescription": description,
9439            "OwnerId": self.account_id,
9440        });
9441        let mut accounts = self.rds_state.write();
9442        let state = accounts.get_or_create(&self.account_id);
9443        rds_extras_mut(state, "security_groups").insert(name.clone(), entry);
9444        Ok(ProvisionResult::new(name))
9445    }
9446
9447    fn delete_rds_security_group(&self, physical_id: &str) -> Result<(), String> {
9448        let mut accounts = self.rds_state.write();
9449        let state = accounts.get_or_create(&self.account_id);
9450        if let Some(m) = state.extras.get_mut("security_groups") {
9451            m.remove(physical_id);
9452        }
9453        Ok(())
9454    }
9455
9456    fn create_rds_db_proxy(
9457        &self,
9458        resource: &ResourceDefinition,
9459    ) -> Result<ProvisionResult, String> {
9460        let props = &resource.properties;
9461        let name = props
9462            .get("DBProxyName")
9463            .and_then(|v| v.as_str())
9464            .unwrap_or(&resource.logical_id)
9465            .to_string();
9466        let engine_family = props
9467            .get("EngineFamily")
9468            .and_then(|v| v.as_str())
9469            .unwrap_or("POSTGRESQL")
9470            .to_string();
9471        let arn = format!(
9472            "arn:aws:rds:{}:{}:db-proxy:{}",
9473            self.region, self.account_id, name
9474        );
9475        let endpoint = format!("{name}.proxy-default.{}.rds.amazonaws.com", self.region);
9476        let entry = serde_json::json!({
9477            "DBProxyName": name,
9478            "DBProxyArn": arn,
9479            "Status": "available",
9480            "EngineFamily": engine_family,
9481            "Endpoint": endpoint,
9482        });
9483        let mut accounts = self.rds_state.write();
9484        let state = accounts.get_or_create(&self.account_id);
9485        rds_extras_mut(state, "proxies").insert(name.clone(), entry);
9486        Ok(ProvisionResult::new(name)
9487            .with("DBProxyArn", arn)
9488            .with("Endpoint", endpoint))
9489    }
9490
9491    fn delete_rds_db_proxy(&self, physical_id: &str) -> Result<(), String> {
9492        let mut accounts = self.rds_state.write();
9493        let state = accounts.get_or_create(&self.account_id);
9494        if let Some(m) = state.extras.get_mut("proxies") {
9495            m.remove(physical_id);
9496        }
9497        Ok(())
9498    }
9499
9500    fn create_rds_db_instance(
9501        &self,
9502        resource: &ResourceDefinition,
9503    ) -> Result<ProvisionResult, String> {
9504        let props = &resource.properties;
9505        let identifier = props
9506            .get("DBInstanceIdentifier")
9507            .and_then(|v| v.as_str())
9508            .map(String::from)
9509            .unwrap_or_else(|| {
9510                format!(
9511                    "cfn-{}-{}",
9512                    resource.logical_id.to_lowercase(),
9513                    Uuid::new_v4().simple().to_string()[..8].to_lowercase()
9514                )
9515            });
9516        let class = props
9517            .get("DBInstanceClass")
9518            .and_then(|v| v.as_str())
9519            .unwrap_or("db.t4g.micro")
9520            .to_string();
9521        let engine = props
9522            .get("Engine")
9523            .and_then(|v| v.as_str())
9524            .unwrap_or("postgres")
9525            .to_string();
9526        let engine_version = props
9527            .get("EngineVersion")
9528            .and_then(|v| v.as_str())
9529            .unwrap_or("16.0")
9530            .to_string();
9531        let master_username = props
9532            .get("MasterUsername")
9533            .and_then(|v| v.as_str())
9534            .unwrap_or("admin")
9535            .to_string();
9536        let master_user_password = props
9537            .get("MasterUserPassword")
9538            .and_then(|v| v.as_str())
9539            .unwrap_or("")
9540            .to_string();
9541        let db_name = props
9542            .get("DBName")
9543            .and_then(|v| v.as_str())
9544            .map(String::from);
9545        let port = props
9546            .get("Port")
9547            .and_then(|v| v.as_i64())
9548            .map(|n| n as i32)
9549            .unwrap_or(5432);
9550        let allocated_storage = props
9551            .get("AllocatedStorage")
9552            .and_then(|v| {
9553                v.as_i64()
9554                    .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
9555            })
9556            .map(|n| n as i32)
9557            .unwrap_or(20);
9558        let publicly_accessible = props
9559            .get("PubliclyAccessible")
9560            .and_then(|v| v.as_bool())
9561            .unwrap_or(false);
9562        let deletion_protection = props
9563            .get("DeletionProtection")
9564            .and_then(|v| v.as_bool())
9565            .unwrap_or(false);
9566        let backup_retention_period = props
9567            .get("BackupRetentionPeriod")
9568            .and_then(|v| v.as_i64())
9569            .map(|n| n as i32)
9570            .unwrap_or(0);
9571        let multi_az = props
9572            .get("MultiAZ")
9573            .and_then(|v| v.as_bool())
9574            .unwrap_or(false);
9575        let availability_zone = props
9576            .get("AvailabilityZone")
9577            .and_then(|v| v.as_str())
9578            .map(String::from);
9579        let storage_type = props
9580            .get("StorageType")
9581            .and_then(|v| v.as_str())
9582            .map(String::from);
9583        let storage_encrypted = props
9584            .get("StorageEncrypted")
9585            .and_then(|v| v.as_bool())
9586            .unwrap_or(false);
9587        let kms_key_id = props
9588            .get("KmsKeyId")
9589            .and_then(|v| v.as_str())
9590            .map(String::from);
9591        let iam_database_authentication_enabled = props
9592            .get("EnableIAMDatabaseAuthentication")
9593            .and_then(|v| v.as_bool())
9594            .unwrap_or(false);
9595        let db_parameter_group_name = props
9596            .get("DBParameterGroupName")
9597            .and_then(|v| v.as_str())
9598            .map(String::from);
9599        let option_group_name = props
9600            .get("OptionGroupName")
9601            .and_then(|v| v.as_str())
9602            .map(String::from);
9603        let vpc_security_group_ids: Vec<String> = props
9604            .get("VPCSecurityGroups")
9605            .and_then(|v| v.as_array())
9606            .map(|arr| {
9607                arr.iter()
9608                    .filter_map(|v| v.as_str().map(String::from))
9609                    .collect()
9610            })
9611            .unwrap_or_default();
9612        let enabled_cloudwatch_logs_exports: Vec<String> = props
9613            .get("EnableCloudwatchLogsExports")
9614            .and_then(|v| v.as_array())
9615            .map(|arr| {
9616                arr.iter()
9617                    .filter_map(|v| v.as_str().map(String::from))
9618                    .collect()
9619            })
9620            .unwrap_or_default();
9621        let tags = parse_rds_tags(props.get("Tags"));
9622
9623        let mut accounts = self.rds_state.write();
9624        let state = accounts.get_or_create(&self.account_id);
9625        let arn = state.db_instance_arn(&identifier);
9626        let endpoint_address = format!(
9627            "{identifier}.cluster-fakecloud.{}.rds.amazonaws.com",
9628            state.region
9629        );
9630        let dbi_resource_id = format!("db-{}", Uuid::new_v4().simple());
9631        let inst = DbInstance {
9632            db_instance_identifier: identifier.clone(),
9633            db_instance_arn: arn.clone(),
9634            db_instance_class: class,
9635            engine,
9636            engine_version,
9637            db_instance_status: "available".to_string(),
9638            master_username,
9639            db_name,
9640            endpoint_address,
9641            port,
9642            allocated_storage,
9643            publicly_accessible,
9644            deletion_protection,
9645            created_at: Utc::now(),
9646            dbi_resource_id,
9647            master_user_password,
9648            container_id: String::new(),
9649            host_port: 0,
9650            tags,
9651            read_replica_source_db_instance_identifier: None,
9652            read_replica_db_instance_identifiers: Vec::new(),
9653            vpc_security_group_ids,
9654            db_parameter_group_name,
9655            backup_retention_period,
9656            preferred_backup_window: "03:00-04:00".to_string(),
9657            preferred_maintenance_window: None,
9658            latest_restorable_time: None,
9659            option_group_name,
9660            multi_az,
9661            pending_modified_values: None,
9662            availability_zone,
9663            storage_type,
9664            storage_encrypted,
9665            kms_key_id,
9666            iam_database_authentication_enabled,
9667            iops: props.get("Iops").and_then(|v| v.as_i64()).map(|n| n as i32),
9668            monitoring_interval: props
9669                .get("MonitoringInterval")
9670                .and_then(|v| v.as_i64())
9671                .map(|n| n as i32),
9672            monitoring_role_arn: props
9673                .get("MonitoringRoleArn")
9674                .and_then(|v| v.as_str())
9675                .map(String::from),
9676            performance_insights_enabled: props
9677                .get("EnablePerformanceInsights")
9678                .and_then(|v| v.as_bool())
9679                .unwrap_or(false),
9680            performance_insights_kms_key_id: props
9681                .get("PerformanceInsightsKMSKeyId")
9682                .and_then(|v| v.as_str())
9683                .map(String::from),
9684            performance_insights_retention_period: props
9685                .get("PerformanceInsightsRetentionPeriod")
9686                .and_then(|v| v.as_i64())
9687                .map(|n| n as i32),
9688            enabled_cloudwatch_logs_exports,
9689            ca_certificate_identifier: props
9690                .get("CACertificateIdentifier")
9691                .and_then(|v| v.as_str())
9692                .map(String::from),
9693            network_type: props
9694                .get("NetworkType")
9695                .and_then(|v| v.as_str())
9696                .map(String::from),
9697            character_set_name: props
9698                .get("CharacterSetName")
9699                .and_then(|v| v.as_str())
9700                .map(String::from),
9701            auto_minor_version_upgrade: props
9702                .get("AutoMinorVersionUpgrade")
9703                .and_then(|v| v.as_bool()),
9704            copy_tags_to_snapshot: props.get("CopyTagsToSnapshot").and_then(|v| v.as_bool()),
9705            master_user_secret_arn: None,
9706            master_user_secret_kms_key_id: props
9707                .get("MasterUserSecret")
9708                .and_then(|v| v.get("KmsKeyId"))
9709                .and_then(|v| v.as_str())
9710                .map(String::from),
9711            license_model: props
9712                .get("LicenseModel")
9713                .and_then(|v| v.as_str())
9714                .map(String::from),
9715            max_allocated_storage: props
9716                .get("MaxAllocatedStorage")
9717                .and_then(|v| v.as_i64())
9718                .map(|n| n as i32),
9719            multi_tenant: props.get("MultiTenant").and_then(|v| v.as_bool()),
9720            storage_throughput: props
9721                .get("StorageThroughput")
9722                .and_then(|v| v.as_i64())
9723                .map(|n| n as i32),
9724            tde_credential_arn: props
9725                .get("TdeCredentialArn")
9726                .and_then(|v| v.as_str())
9727                .map(String::from),
9728            delete_automated_backups: props
9729                .get("DeleteAutomatedBackups")
9730                .and_then(|v| v.as_bool()),
9731            db_security_groups: props
9732                .get("DBSecurityGroups")
9733                .and_then(|v| v.as_array())
9734                .map(|arr| {
9735                    arr.iter()
9736                        .filter_map(|v| v.as_str().map(String::from))
9737                        .collect()
9738                })
9739                .unwrap_or_default(),
9740            domain: props
9741                .get("Domain")
9742                .and_then(|v| v.as_str())
9743                .map(String::from),
9744            domain_fqdn: props
9745                .get("DomainFqdn")
9746                .and_then(|v| v.as_str())
9747                .map(String::from),
9748            domain_ou: props
9749                .get("DomainOu")
9750                .and_then(|v| v.as_str())
9751                .map(String::from),
9752            domain_iam_role_name: props
9753                .get("DomainIAMRoleName")
9754                .and_then(|v| v.as_str())
9755                .map(String::from),
9756            domain_auth_secret_arn: props
9757                .get("DomainAuthSecretArn")
9758                .and_then(|v| v.as_str())
9759                .map(String::from),
9760            domain_dns_ips: props
9761                .get("DomainDnsIps")
9762                .and_then(|v| v.as_array())
9763                .map(|arr| {
9764                    arr.iter()
9765                        .filter_map(|v| v.as_str().map(String::from))
9766                        .collect()
9767                })
9768                .unwrap_or_default(),
9769            db_cluster_identifier: props
9770                .get("DBClusterIdentifier")
9771                .and_then(|v| v.as_str())
9772                .map(String::from),
9773        };
9774        let endpoint = inst.endpoint_address.clone();
9775        let endpoint_port = inst.port;
9776        state.instances.insert(identifier.clone(), inst);
9777
9778        Ok(ProvisionResult::new(identifier.clone())
9779            .with("DBInstanceArn", arn)
9780            .with("Endpoint.Address", endpoint)
9781            .with("Endpoint.Port", endpoint_port.to_string())
9782            .with("DbiResourceId", format!("db-{identifier}")))
9783    }
9784
9785    fn delete_rds_db_instance(&self, physical_id: &str) -> Result<(), String> {
9786        let mut accounts = self.rds_state.write();
9787        let state = accounts.get_or_create(&self.account_id);
9788        state.instances.remove(physical_id);
9789        Ok(())
9790    }
9791
9792    fn create_rds_db_cluster(
9793        &self,
9794        resource: &ResourceDefinition,
9795    ) -> Result<ProvisionResult, String> {
9796        let props = &resource.properties;
9797        let identifier = props
9798            .get("DBClusterIdentifier")
9799            .and_then(|v| v.as_str())
9800            .map(String::from)
9801            .unwrap_or_else(|| {
9802                format!(
9803                    "cfn-cluster-{}-{}",
9804                    resource.logical_id.to_lowercase(),
9805                    Uuid::new_v4().simple().to_string()[..8].to_lowercase()
9806                )
9807            });
9808        let engine = props
9809            .get("Engine")
9810            .and_then(|v| v.as_str())
9811            .unwrap_or("aurora-postgresql")
9812            .to_string();
9813        let engine_version = props
9814            .get("EngineVersion")
9815            .and_then(|v| v.as_str())
9816            .map(String::from);
9817        let master_username = props
9818            .get("MasterUsername")
9819            .and_then(|v| v.as_str())
9820            .map(String::from);
9821        let port = props.get("Port").and_then(|v| v.as_i64()).unwrap_or(5432);
9822        let mut accounts = self.rds_state.write();
9823        let state = accounts.get_or_create(&self.account_id);
9824        let arn = format!(
9825            "arn:aws:rds:{}:{}:cluster:{}",
9826            state.region, state.account_id, identifier
9827        );
9828        let cluster_resource_id = format!("cluster-{}", Uuid::new_v4().simple());
9829        let endpoint = format!(
9830            "{identifier}.cluster-fakecloud.{}.rds.amazonaws.com",
9831            state.region
9832        );
9833        let reader_endpoint = format!(
9834            "{identifier}.cluster-ro-fakecloud.{}.rds.amazonaws.com",
9835            state.region
9836        );
9837        let body = serde_json::json!({
9838            "DBClusterIdentifier": identifier,
9839            "DBClusterArn": arn,
9840            "Engine": engine,
9841            "EngineVersion": engine_version,
9842            "MasterUsername": master_username,
9843            "Status": "available",
9844            "DbClusterResourceId": cluster_resource_id,
9845            "Endpoint": endpoint,
9846            "ReaderEndpoint": reader_endpoint,
9847            "Port": port,
9848            "AllocatedStorage": props.get("AllocatedStorage").and_then(|v| v.as_i64()).unwrap_or(1),
9849            "BackupRetentionPeriod": props.get("BackupRetentionPeriod").and_then(|v| v.as_i64()).unwrap_or(1),
9850            "DatabaseName": props.get("DatabaseName").and_then(|v| v.as_str()),
9851            "DBSubnetGroup": props.get("DBSubnetGroupName").and_then(|v| v.as_str()),
9852            "VpcSecurityGroupIds": props.get("VpcSecurityGroupIds").cloned().unwrap_or(serde_json::json!([])),
9853            "StorageEncrypted": props.get("StorageEncrypted").and_then(|v| v.as_bool()).unwrap_or(false),
9854            "KmsKeyId": props.get("KmsKeyId").and_then(|v| v.as_str()),
9855            "DeletionProtection": props.get("DeletionProtection").and_then(|v| v.as_bool()).unwrap_or(false),
9856            "ClusterCreateTime": Utc::now().to_rfc3339(),
9857            "EnabledCloudwatchLogsExports": props.get("EnableCloudwatchLogsExports").cloned().unwrap_or(serde_json::json!([])),
9858            "MultiAZ": false,
9859            "DBClusterMembers": [],
9860        });
9861        state
9862            .extras
9863            .entry("clusters".to_string())
9864            .or_default()
9865            .insert(identifier.clone(), body);
9866        Ok(ProvisionResult::new(identifier.clone())
9867            .with("DBClusterArn", arn)
9868            .with("Endpoint.Address", endpoint)
9869            .with("ReadEndpoint.Address", reader_endpoint)
9870            .with("Endpoint.Port", port.to_string())
9871            .with("DBClusterResourceId", cluster_resource_id))
9872    }
9873
9874    fn delete_rds_db_cluster(&self, physical_id: &str) -> Result<(), String> {
9875        let mut accounts = self.rds_state.write();
9876        let state = accounts.get_or_create(&self.account_id);
9877        if let Some(m) = state.extras.get_mut("clusters") {
9878            m.remove(physical_id);
9879        }
9880        Ok(())
9881    }
9882
9883    // --- ECS ---
9884
9885    fn create_ecs_cluster(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
9886        let props = &resource.properties;
9887        let cluster_name = props
9888            .get("ClusterName")
9889            .and_then(|v| v.as_str())
9890            .unwrap_or(&resource.logical_id)
9891            .to_string();
9892        let cluster_arn = format!(
9893            "arn:aws:ecs:{}:{}:cluster/{}",
9894            self.region, self.account_id, cluster_name
9895        );
9896        let mut cluster = EcsCluster::new(&cluster_name, cluster_arn.clone());
9897        cluster.tags = parse_ecs_tags(props.get("Tags"));
9898        cluster.capacity_providers = props
9899            .get("CapacityProviders")
9900            .and_then(|v| v.as_array())
9901            .map(|arr| {
9902                arr.iter()
9903                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
9904                    .collect()
9905            })
9906            .unwrap_or_default();
9907        if let Some(strategy) = props
9908            .get("DefaultCapacityProviderStrategy")
9909            .and_then(|v| v.as_array())
9910        {
9911            cluster.default_capacity_provider_strategy =
9912                strategy.iter().cloned().map(lowercase_first_keys).collect();
9913        }
9914        if let Some(cfg) = props.get("Configuration") {
9915            cluster.configuration = Some(lowercase_first_keys(cfg.clone()));
9916        }
9917        if let Some(settings) = props.get("ClusterSettings").and_then(|v| v.as_array()) {
9918            cluster.settings = settings.iter().cloned().map(lowercase_first_keys).collect();
9919        }
9920        if let Some(scd) = props.get("ServiceConnectDefaults") {
9921            cluster.service_connect_defaults = Some(lowercase_first_keys(scd.clone()));
9922        }
9923        let mut accounts = self.ecs_state.write();
9924        let state = accounts.get_or_create(&self.account_id);
9925        state.clusters.insert(cluster_name.clone(), cluster);
9926        Ok(ProvisionResult::new(cluster_name).with("Arn", cluster_arn))
9927    }
9928
9929    fn delete_ecs_cluster(&self, physical_id: &str) -> Result<(), String> {
9930        let mut accounts = self.ecs_state.write();
9931        let state = accounts.get_or_create(&self.account_id);
9932        state.clusters.remove(physical_id);
9933        // Cascade: drop services + tasks tied to this cluster.
9934        state.services.retain(|_, s| s.cluster_name != physical_id);
9935        state
9936            .tasks
9937            .retain(|_, t| t.cluster_arn.split('/').next_back() != Some(physical_id));
9938        Ok(())
9939    }
9940
9941    fn create_ecs_task_definition(
9942        &self,
9943        resource: &ResourceDefinition,
9944    ) -> Result<ProvisionResult, String> {
9945        let props = &resource.properties;
9946        let family = props
9947            .get("Family")
9948            .and_then(|v| v.as_str())
9949            .unwrap_or(&resource.logical_id)
9950            .to_string();
9951        // ECS DescribeTaskDefinition emits camelCase keys; CFN ships PascalCase.
9952        // Recursively lower the leading char so SDKs deserialize cleanly.
9953        let container_definitions: Vec<serde_json::Value> = props
9954            .get("ContainerDefinitions")
9955            .and_then(|v| v.as_array())
9956            .cloned()
9957            .unwrap_or_default()
9958            .into_iter()
9959            .map(lowercase_first_keys)
9960            .collect();
9961        let task_role_arn = props
9962            .get("TaskRoleArn")
9963            .and_then(|v| v.as_str())
9964            .map(|s| s.to_string());
9965        let execution_role_arn = props
9966            .get("ExecutionRoleArn")
9967            .and_then(|v| v.as_str())
9968            .map(|s| s.to_string());
9969        let network_mode = props
9970            .get("NetworkMode")
9971            .and_then(|v| v.as_str())
9972            .map(|s| s.to_string());
9973        let requires_compatibilities: Vec<String> = props
9974            .get("RequiresCompatibilities")
9975            .and_then(|v| v.as_array())
9976            .map(|arr| {
9977                arr.iter()
9978                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
9979                    .collect()
9980            })
9981            .unwrap_or_default();
9982        let cpu = props
9983            .get("Cpu")
9984            .and_then(|v| v.as_str())
9985            .map(|s| s.to_string());
9986        let memory = props
9987            .get("Memory")
9988            .and_then(|v| v.as_str())
9989            .map(|s| s.to_string());
9990        let volumes: Vec<serde_json::Value> = props
9991            .get("Volumes")
9992            .and_then(|v| v.as_array())
9993            .cloned()
9994            .unwrap_or_default()
9995            .into_iter()
9996            .map(lowercase_first_keys)
9997            .collect();
9998        let placement_constraints: Vec<serde_json::Value> = props
9999            .get("PlacementConstraints")
10000            .and_then(|v| v.as_array())
10001            .cloned()
10002            .unwrap_or_default()
10003            .into_iter()
10004            .map(lowercase_first_keys)
10005            .collect();
10006        let proxy_configuration = props
10007            .get("ProxyConfiguration")
10008            .cloned()
10009            .map(lowercase_first_keys);
10010        let ephemeral_storage = props
10011            .get("EphemeralStorage")
10012            .cloned()
10013            .map(lowercase_first_keys);
10014        let runtime_platform = props
10015            .get("RuntimePlatform")
10016            .cloned()
10017            .map(lowercase_first_keys);
10018        let tags = parse_ecs_tags(props.get("Tags"));
10019
10020        let mut accounts = self.ecs_state.write();
10021        let state = accounts.get_or_create(&self.account_id);
10022        let revision = state
10023            .next_revision
10024            .entry(family.clone())
10025            .and_modify(|n| *n += 1)
10026            .or_insert(1);
10027        let revision = *revision;
10028        let arn = format!(
10029            "arn:aws:ecs:{}:{}:task-definition/{}:{}",
10030            self.region, self.account_id, family, revision
10031        );
10032        let td = EcsTaskDefinition {
10033            family: family.clone(),
10034            revision,
10035            task_definition_arn: arn.clone(),
10036            container_definitions,
10037            status: "ACTIVE".to_string(),
10038            task_role_arn,
10039            execution_role_arn,
10040            network_mode,
10041            requires_compatibilities: requires_compatibilities.clone(),
10042            compatibilities: requires_compatibilities,
10043            cpu,
10044            memory,
10045            pid_mode: None,
10046            ipc_mode: None,
10047            volumes,
10048            placement_constraints,
10049            proxy_configuration,
10050            inference_accelerators: Vec::new(),
10051            ephemeral_storage,
10052            runtime_platform,
10053            requires_attributes: Vec::new(),
10054            registered_at: Utc::now(),
10055            registered_by: None,
10056            deregistered_at: None,
10057            tags,
10058            enable_fault_injection: props.get("EnableFaultInjection").and_then(|v| v.as_bool()),
10059        };
10060        state
10061            .task_definitions
10062            .entry(family.clone())
10063            .or_default()
10064            .insert(revision, td);
10065        Ok(ProvisionResult::new(arn.clone()).with("TaskDefinitionArn", arn))
10066    }
10067
10068    fn delete_ecs_task_definition(&self, physical_id: &str) -> Result<(), String> {
10069        // physical_id is the full task-definition ARN; family + revision
10070        // sit at the trailing segment after `/`.
10071        let Some(suffix) = physical_id.rsplit('/').next() else {
10072            return Ok(());
10073        };
10074        let Some((family, rev)) = suffix.split_once(':') else {
10075            return Ok(());
10076        };
10077        let Ok(revision) = rev.parse::<i32>() else {
10078            return Ok(());
10079        };
10080        let mut accounts = self.ecs_state.write();
10081        let state = accounts.get_or_create(&self.account_id);
10082        if let Some(revs) = state.task_definitions.get_mut(family) {
10083            if let Some(td) = revs.get_mut(&revision) {
10084                td.status = "INACTIVE".to_string();
10085                td.deregistered_at = Some(Utc::now());
10086            }
10087        }
10088        Ok(())
10089    }
10090
10091    fn create_ecs_service(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
10092        let props = &resource.properties;
10093        let service_name = props
10094            .get("ServiceName")
10095            .and_then(|v| v.as_str())
10096            .unwrap_or(&resource.logical_id)
10097            .to_string();
10098        // Cluster: default to "default" if missing; accept name or ARN.
10099        let cluster_name = props
10100            .get("Cluster")
10101            .and_then(|v| v.as_str())
10102            .map(parse_ecs_cluster_name)
10103            .unwrap_or_else(|| "default".to_string());
10104        let task_definition_arn = props
10105            .get("TaskDefinition")
10106            .and_then(|v| v.as_str())
10107            .ok_or_else(|| "TaskDefinition is required".to_string())?
10108            .to_string();
10109        let desired_count = props
10110            .get("DesiredCount")
10111            .and_then(cfn_as_i64)
10112            .map(|n| n as i32)
10113            .unwrap_or(1);
10114        let launch_type = props
10115            .get("LaunchType")
10116            .and_then(|v| v.as_str())
10117            .unwrap_or("FARGATE")
10118            .to_string();
10119        let scheduling_strategy = props
10120            .get("SchedulingStrategy")
10121            .and_then(|v| v.as_str())
10122            .unwrap_or("REPLICA")
10123            .to_string();
10124        let deployment_controller = props
10125            .get("DeploymentController")
10126            .and_then(|v| v.get("Type"))
10127            .and_then(|v| v.as_str())
10128            .unwrap_or("ECS")
10129            .to_string();
10130        let load_balancers: Vec<serde_json::Value> = props
10131            .get("LoadBalancers")
10132            .and_then(|v| v.as_array())
10133            .cloned()
10134            .unwrap_or_default()
10135            .into_iter()
10136            .map(lowercase_first_keys)
10137            .collect();
10138        let service_registries: Vec<serde_json::Value> = props
10139            .get("ServiceRegistries")
10140            .and_then(|v| v.as_array())
10141            .cloned()
10142            .unwrap_or_default()
10143            .into_iter()
10144            .map(lowercase_first_keys)
10145            .collect();
10146        let placement_constraints: Vec<serde_json::Value> = props
10147            .get("PlacementConstraints")
10148            .and_then(|v| v.as_array())
10149            .cloned()
10150            .unwrap_or_default()
10151            .into_iter()
10152            .map(lowercase_first_keys)
10153            .collect();
10154        let placement_strategy: Vec<serde_json::Value> = props
10155            .get("PlacementStrategies")
10156            .and_then(|v| v.as_array())
10157            .cloned()
10158            .unwrap_or_default()
10159            .into_iter()
10160            .map(lowercase_first_keys)
10161            .collect();
10162        let network_configuration = props
10163            .get("NetworkConfiguration")
10164            .cloned()
10165            .map(lowercase_first_keys);
10166        let role_arn = props
10167            .get("Role")
10168            .and_then(|v| v.as_str())
10169            .map(|s| s.to_string());
10170        let platform_version = props
10171            .get("PlatformVersion")
10172            .and_then(|v| v.as_str())
10173            .map(|s| s.to_string());
10174        let health_check_grace_period_seconds = props
10175            .get("HealthCheckGracePeriodSeconds")
10176            .and_then(cfn_as_i64)
10177            .map(|n| n as i32);
10178        let enable_ecs_managed_tags = props
10179            .get("EnableECSManagedTags")
10180            .and_then(|v| v.as_bool())
10181            .unwrap_or(false);
10182        let enable_execute_command = props
10183            .get("EnableExecuteCommand")
10184            .and_then(|v| v.as_bool())
10185            .unwrap_or(false);
10186        let propagate_tags = props
10187            .get("PropagateTags")
10188            .and_then(|v| v.as_str())
10189            .map(|s| s.to_string());
10190        let capacity_provider_strategy: Vec<serde_json::Value> = props
10191            .get("CapacityProviderStrategy")
10192            .and_then(|v| v.as_array())
10193            .cloned()
10194            .unwrap_or_default()
10195            .into_iter()
10196            .map(lowercase_first_keys)
10197            .collect();
10198        let availability_zone_rebalancing = props
10199            .get("AvailabilityZoneRebalancing")
10200            .and_then(|v| v.as_str())
10201            .map(|s| s.to_string());
10202        let tags = parse_ecs_tags(props.get("Tags"));
10203
10204        // Family + revision parsed from the task definition ARN tail.
10205        let (family, revision) = parse_td_arn(&task_definition_arn);
10206
10207        let mut accounts = self.ecs_state.write();
10208        let state = accounts.get_or_create(&self.account_id);
10209        if !state.clusters.contains_key(&cluster_name) {
10210            return Err(format!(
10211                "Cluster {cluster_name} does not exist yet — retry once it has been provisioned"
10212            ));
10213        }
10214        let cluster_arn = state.clusters[&cluster_name].cluster_arn.clone();
10215        let service_arn = format!(
10216            "arn:aws:ecs:{}:{}:service/{}/{}",
10217            self.region, self.account_id, cluster_name, service_name
10218        );
10219        let key = format!("{cluster_name}/{service_name}");
10220        let service = EcsService {
10221            service_name: service_name.clone(),
10222            service_arn: service_arn.clone(),
10223            cluster_name: cluster_name.clone(),
10224            cluster_arn,
10225            task_definition_arn,
10226            family,
10227            revision,
10228            desired_count,
10229            running_count: 0,
10230            pending_count: 0,
10231            launch_type,
10232            status: "ACTIVE".to_string(),
10233            scheduling_strategy,
10234            deployment_controller,
10235            minimum_healthy_percent: props
10236                .get("DeploymentConfiguration")
10237                .and_then(|v| v.get("MinimumHealthyPercent"))
10238                .and_then(cfn_as_i64)
10239                .map(|n| n as i32),
10240            maximum_percent: props
10241                .get("DeploymentConfiguration")
10242                .and_then(|v| v.get("MaximumPercent"))
10243                .and_then(cfn_as_i64)
10244                .map(|n| n as i32),
10245            circuit_breaker: None,
10246            deployments: Vec::new(),
10247            load_balancers,
10248            service_registries,
10249            placement_constraints,
10250            placement_strategy,
10251            network_configuration,
10252            tags,
10253            created_at: Utc::now(),
10254            created_by: None,
10255            role_arn,
10256            platform_version,
10257            health_check_grace_period_seconds,
10258            enable_execute_command,
10259            enable_ecs_managed_tags,
10260            propagate_tags,
10261            capacity_provider_strategy,
10262            availability_zone_rebalancing,
10263            volume_configurations: Vec::new(),
10264        };
10265        state.services.insert(key.clone(), service);
10266        if let Some(c) = state.clusters.get_mut(&cluster_name) {
10267            c.active_services_count += 1;
10268        }
10269        Ok(ProvisionResult::new(service_arn.clone())
10270            .with("Name", service_name)
10271            .with("ServiceArn", service_arn))
10272    }
10273
10274    fn delete_ecs_service(&self, physical_id: &str) -> Result<(), String> {
10275        // physical_id is full Service ARN: .../service/<cluster>/<service>
10276        let Some((cluster, service)) = parse_service_arn(physical_id) else {
10277            return Ok(());
10278        };
10279        let key = format!("{cluster}/{service}");
10280        let mut accounts = self.ecs_state.write();
10281        let state = accounts.get_or_create(&self.account_id);
10282        if state.services.remove(&key).is_some() {
10283            if let Some(c) = state.clusters.get_mut(&cluster) {
10284                if c.active_services_count > 0 {
10285                    c.active_services_count -= 1;
10286                }
10287            }
10288        }
10289        Ok(())
10290    }
10291
10292    fn create_ecs_capacity_provider(
10293        &self,
10294        resource: &ResourceDefinition,
10295    ) -> Result<ProvisionResult, String> {
10296        let props = &resource.properties;
10297        let name = props
10298            .get("Name")
10299            .and_then(|v| v.as_str())
10300            .unwrap_or(&resource.logical_id)
10301            .to_string();
10302        let arn = format!(
10303            "arn:aws:ecs:{}:{}:capacity-provider/{}",
10304            self.region, self.account_id, name
10305        );
10306        let cp = EcsCapacityProvider {
10307            name: name.clone(),
10308            arn: arn.clone(),
10309            status: "ACTIVE".to_string(),
10310            auto_scaling_group_provider: props.get("AutoScalingGroupProvider").cloned(),
10311            update_status: None,
10312            update_status_reason: None,
10313            created_at: Utc::now(),
10314            tags: parse_ecs_tags(props.get("Tags")),
10315        };
10316        let mut accounts = self.ecs_state.write();
10317        let state = accounts.get_or_create(&self.account_id);
10318        state.capacity_providers.insert(name.clone(), cp);
10319        Ok(ProvisionResult::new(name).with("Arn", arn))
10320    }
10321
10322    fn delete_ecs_capacity_provider(&self, physical_id: &str) -> Result<(), String> {
10323        let mut accounts = self.ecs_state.write();
10324        let state = accounts.get_or_create(&self.account_id);
10325        state.capacity_providers.remove(physical_id);
10326        Ok(())
10327    }
10328
10329    /// In-place update for AWS::ECS::Cluster. ClusterName is immutable —
10330    /// CFN replaces the resource if it changes — so we only refresh
10331    /// settings/configuration/capacity-provider/tags here. The physical
10332    /// id is kept stable.
10333    fn update_ecs_cluster(
10334        &self,
10335        existing: &StackResource,
10336        resource: &ResourceDefinition,
10337    ) -> Result<ProvisionResult, String> {
10338        let props = &resource.properties;
10339        let cluster_name = existing.physical_id.clone();
10340        let mut accounts = self.ecs_state.write();
10341        let state = accounts.get_or_create(&self.account_id);
10342        let cluster = state
10343            .clusters
10344            .get_mut(&cluster_name)
10345            .ok_or_else(|| format!("Cluster {cluster_name} no longer exists"))?;
10346        if let Some(settings) = props.get("ClusterSettings").and_then(|v| v.as_array()) {
10347            cluster.settings = settings.iter().cloned().map(lowercase_first_keys).collect();
10348        }
10349        if let Some(cfg) = props.get("Configuration") {
10350            cluster.configuration = Some(lowercase_first_keys(cfg.clone()));
10351        }
10352        if let Some(cps) = props.get("CapacityProviders").and_then(|v| v.as_array()) {
10353            cluster.capacity_providers = cps
10354                .iter()
10355                .filter_map(|v| v.as_str().map(|s| s.to_string()))
10356                .collect();
10357        }
10358        if let Some(strategy) = props
10359            .get("DefaultCapacityProviderStrategy")
10360            .and_then(|v| v.as_array())
10361        {
10362            cluster.default_capacity_provider_strategy =
10363                strategy.iter().cloned().map(lowercase_first_keys).collect();
10364        }
10365        if let Some(scd) = props.get("ServiceConnectDefaults") {
10366            cluster.service_connect_defaults = Some(lowercase_first_keys(scd.clone()));
10367        }
10368        if props.get("Tags").is_some() {
10369            cluster.tags = parse_ecs_tags(props.get("Tags"));
10370        }
10371        let cluster_arn = cluster.cluster_arn.clone();
10372        Ok(ProvisionResult::new(cluster_name).with("Arn", cluster_arn))
10373    }
10374
10375    /// In-place update for AWS::ECS::Service. Service ARN is keyed by
10376    /// cluster + service name, both immutable. CFN replaces the resource
10377    /// if either changes, so we only mutate task definition / desired
10378    /// count / deployment-related fields here.
10379    fn update_ecs_service(
10380        &self,
10381        existing: &StackResource,
10382        resource: &ResourceDefinition,
10383    ) -> Result<ProvisionResult, String> {
10384        let props = &resource.properties;
10385        let service_arn = existing.physical_id.clone();
10386        let Some((cluster_name, service_name)) = parse_service_arn(&service_arn) else {
10387            return Err(format!("Cannot parse service ARN: {service_arn}"));
10388        };
10389        let key = format!("{cluster_name}/{service_name}");
10390        let mut accounts = self.ecs_state.write();
10391        let state = accounts.get_or_create(&self.account_id);
10392        let svc = state
10393            .services
10394            .get_mut(&key)
10395            .ok_or_else(|| format!("Service {service_arn} no longer exists"))?;
10396        if let Some(td) = props.get("TaskDefinition").and_then(|v| v.as_str()) {
10397            svc.task_definition_arn = td.to_string();
10398            let (family, revision) = parse_td_arn(td);
10399            svc.family = family;
10400            svc.revision = revision;
10401        }
10402        if let Some(n) = props.get("DesiredCount").and_then(cfn_as_i64) {
10403            svc.desired_count = n as i32;
10404        }
10405        if let Some(s) = props.get("LaunchType").and_then(|v| v.as_str()) {
10406            svc.launch_type = s.to_string();
10407        }
10408        if let Some(s) = props.get("PlatformVersion").and_then(|v| v.as_str()) {
10409            svc.platform_version = Some(s.to_string());
10410        }
10411        if let Some(n) = props
10412            .get("HealthCheckGracePeriodSeconds")
10413            .and_then(cfn_as_i64)
10414        {
10415            svc.health_check_grace_period_seconds = Some(n as i32);
10416        }
10417        if let Some(b) = props.get("EnableExecuteCommand").and_then(|v| v.as_bool()) {
10418            svc.enable_execute_command = b;
10419        }
10420        if let Some(b) = props.get("EnableECSManagedTags").and_then(|v| v.as_bool()) {
10421            svc.enable_ecs_managed_tags = b;
10422        }
10423        if let Some(s) = props.get("PropagateTags").and_then(|v| v.as_str()) {
10424            svc.propagate_tags = Some(s.to_string());
10425        }
10426        if let Some(s) = props
10427            .get("AvailabilityZoneRebalancing")
10428            .and_then(|v| v.as_str())
10429        {
10430            svc.availability_zone_rebalancing = Some(s.to_string());
10431        }
10432        if let Some(arr) = props
10433            .get("CapacityProviderStrategy")
10434            .and_then(|v| v.as_array())
10435        {
10436            svc.capacity_provider_strategy =
10437                arr.iter().cloned().map(lowercase_first_keys).collect();
10438        }
10439        if let Some(dc) = props.get("DeploymentConfiguration") {
10440            if let Some(n) = dc.get("MinimumHealthyPercent").and_then(cfn_as_i64) {
10441                svc.minimum_healthy_percent = Some(n as i32);
10442            }
10443            if let Some(n) = dc.get("MaximumPercent").and_then(cfn_as_i64) {
10444                svc.maximum_percent = Some(n as i32);
10445            }
10446        }
10447        if let Some(arr) = props.get("LoadBalancers").and_then(|v| v.as_array()) {
10448            svc.load_balancers = arr.iter().cloned().map(lowercase_first_keys).collect();
10449        }
10450        if let Some(arr) = props.get("ServiceRegistries").and_then(|v| v.as_array()) {
10451            svc.service_registries = arr.iter().cloned().map(lowercase_first_keys).collect();
10452        }
10453        if let Some(arr) = props.get("PlacementConstraints").and_then(|v| v.as_array()) {
10454            svc.placement_constraints = arr.iter().cloned().map(lowercase_first_keys).collect();
10455        }
10456        if let Some(arr) = props.get("PlacementStrategies").and_then(|v| v.as_array()) {
10457            svc.placement_strategy = arr.iter().cloned().map(lowercase_first_keys).collect();
10458        }
10459        if let Some(nc) = props.get("NetworkConfiguration") {
10460            svc.network_configuration = Some(lowercase_first_keys(nc.clone()));
10461        }
10462        if props.get("Tags").is_some() {
10463            svc.tags = parse_ecs_tags(props.get("Tags"));
10464        }
10465        let name = svc.service_name.clone();
10466        Ok(ProvisionResult::new(service_arn.clone())
10467            .with("Name", name)
10468            .with("ServiceArn", service_arn))
10469    }
10470
10471    /// In-place update for AWS::ECS::TaskDefinition. ECS treats every
10472    /// register call as a new revision, so a CFN update produces a fresh
10473    /// revision and the physical id (the full ARN) shifts. Returning a
10474    /// new ARN tells CFN's update path that the resource was replaced.
10475    fn update_ecs_task_definition(
10476        &self,
10477        _existing: &StackResource,
10478        resource: &ResourceDefinition,
10479    ) -> Result<ProvisionResult, String> {
10480        // Delegating to create_ecs_task_definition is safe because each
10481        // call bumps the revision counter — semantically the right
10482        // behaviour for ECS.
10483        self.create_ecs_task_definition(resource)
10484    }
10485
10486    /// In-place update for AWS::ECS::CapacityProvider. Name is immutable;
10487    /// only the underlying ASG provider config and tags can change.
10488    fn update_ecs_capacity_provider(
10489        &self,
10490        existing: &StackResource,
10491        resource: &ResourceDefinition,
10492    ) -> Result<ProvisionResult, String> {
10493        let props = &resource.properties;
10494        let name = existing.physical_id.clone();
10495        let mut accounts = self.ecs_state.write();
10496        let state = accounts.get_or_create(&self.account_id);
10497        let cp = state
10498            .capacity_providers
10499            .get_mut(&name)
10500            .ok_or_else(|| format!("CapacityProvider {name} no longer exists"))?;
10501        if props.get("AutoScalingGroupProvider").is_some() {
10502            cp.auto_scaling_group_provider = props.get("AutoScalingGroupProvider").cloned();
10503        }
10504        if props.get("Tags").is_some() {
10505            cp.tags = parse_ecs_tags(props.get("Tags"));
10506        }
10507        let arn = cp.arn.clone();
10508        Ok(ProvisionResult::new(name).with("Arn", arn))
10509    }
10510
10511    fn get_att_ecs_cluster(&self, physical_id: &str, attribute: &str) -> Option<String> {
10512        let mut accounts = self.ecs_state.write();
10513        let state = accounts.get_or_create(&self.account_id);
10514        let cluster = state.clusters.get(physical_id)?;
10515        match attribute {
10516            "Arn" => Some(cluster.cluster_arn.clone()),
10517            _ => None,
10518        }
10519    }
10520
10521    fn get_att_ecs_service(&self, physical_id: &str, attribute: &str) -> Option<String> {
10522        let (cluster, service) = parse_service_arn(physical_id)?;
10523        let key = format!("{cluster}/{service}");
10524        let mut accounts = self.ecs_state.write();
10525        let state = accounts.get_or_create(&self.account_id);
10526        let svc = state.services.get(&key)?;
10527        match attribute {
10528            "Name" => Some(svc.service_name.clone()),
10529            "ServiceArn" => Some(svc.service_arn.clone()),
10530            _ => None,
10531        }
10532    }
10533
10534    fn get_att_ecs_capacity_provider(&self, physical_id: &str, attribute: &str) -> Option<String> {
10535        let mut accounts = self.ecs_state.write();
10536        let state = accounts.get_or_create(&self.account_id);
10537        let cp = state.capacity_providers.get(physical_id)?;
10538        match attribute {
10539            "Arn" => Some(cp.arn.clone()),
10540            _ => None,
10541        }
10542    }
10543
10544    // --- ACM ---
10545
10546    fn create_acm_certificate(
10547        &self,
10548        resource: &ResourceDefinition,
10549    ) -> Result<ProvisionResult, String> {
10550        let props = &resource.properties;
10551        let domain_name = props
10552            .get("DomainName")
10553            .and_then(|v| v.as_str())
10554            .ok_or_else(|| "DomainName is required".to_string())?
10555            .to_string();
10556        let sans: Vec<String> = props
10557            .get("SubjectAlternativeNames")
10558            .and_then(|v| v.as_array())
10559            .map(|arr| {
10560                arr.iter()
10561                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
10562                    .collect()
10563            })
10564            .unwrap_or_default();
10565        let key_algorithm = props
10566            .get("KeyAlgorithm")
10567            .and_then(|v| v.as_str())
10568            .unwrap_or("RSA_2048")
10569            .to_string();
10570        let validation_method = props
10571            .get("ValidationMethod")
10572            .and_then(|v| v.as_str())
10573            .unwrap_or("DNS")
10574            .to_string();
10575        let ca_arn = props
10576            .get("CertificateAuthorityArn")
10577            .and_then(|v| v.as_str())
10578            .map(|s| s.to_string());
10579        let tags = parse_acm_tags(props.get("Tags"));
10580        let cert_transparency = props
10581            .get("CertificateTransparencyLoggingPreference")
10582            .and_then(|v| v.as_str())
10583            .unwrap_or("ENABLED")
10584            .to_string();
10585
10586        // Mint a deterministic-ish ARN — ACM uses a UUID per certificate.
10587        let arn = format!(
10588            "arn:aws:acm:{}:{}:certificate/{}",
10589            self.region,
10590            self.account_id,
10591            Uuid::new_v4()
10592        );
10593        let now = Utc::now();
10594
10595        // Build a real self-signed PEM via rcgen for the cert+SANs so
10596        // GetCertificate / DescribeCertificate round-trip parseable
10597        // X.509 (matches the runtime RequestCertificate path).
10598        let mut all_names = vec![domain_name.clone()];
10599        for s in &sans {
10600            if !all_names.contains(s) {
10601                all_names.push(s.clone());
10602            }
10603        }
10604        let (cert_pem, key_pem) = rcgen::generate_simple_self_signed(all_names.clone())
10605            .map(|c| (c.cert.pem(), c.key_pair.serialize_pem()))
10606            .map(|(c, k)| (Some(c), Some(k)))
10607            .unwrap_or((None, None));
10608
10609        // CFN-provisioned certs land as ISSUED right away — real CFN
10610        // blocks until validation completes, but fakecloud doesn't run
10611        // a DNS server, so leaving the cert PENDING_VALIDATION would
10612        // wedge dependent resources (CloudFront/ELBv2) forever. The
10613        // runtime RequestCertificate path uses the async auto-issue
10614        // tick (DNS) or admin `/approve` endpoint (EMAIL) for parity
10615        // with ACM's async behaviour.
10616        let domain_validation: Vec<AcmDomainValidation> =
10617            synth_acm_domain_validation(&domain_name, &sans, &validation_method)
10618                .into_iter()
10619                .map(|mut dv| {
10620                    dv.validation_status = "SUCCESS".to_string();
10621                    dv
10622                })
10623                .collect();
10624        let renewal_summary = Some(AcmRenewalSummary {
10625            renewal_status: "PENDING_AUTO_RENEWAL".to_string(),
10626            domain_validation: domain_validation.clone(),
10627            renewal_status_reason: None,
10628            updated_at: now,
10629        });
10630        let cert = AcmStoredCertificate {
10631            arn: arn.clone(),
10632            domain_name: domain_name.clone(),
10633            subject_alternative_names: all_names,
10634            status: "ISSUED".to_string(),
10635            cert_type: "AMAZON_ISSUED".to_string(),
10636            certificate_pem: cert_pem,
10637            certificate_chain_pem: None,
10638            private_key_pem: key_pem,
10639            idempotency_token: None,
10640            serial: format!("{:032x}", Uuid::new_v4().as_u128()),
10641            subject: format!("CN={domain_name}"),
10642            issuer: "Amazon".to_string(),
10643            key_algorithm,
10644            signature_algorithm: "SHA256WITHRSA".to_string(),
10645            created_at: now,
10646            issued_at: Some(now),
10647            imported_at: None,
10648            revoked_at: None,
10649            revocation_reason: None,
10650            not_before: now,
10651            // 13 months (matches real ACM issued-cert lifetime).
10652            not_after: now + chrono::Duration::days(395),
10653            validation_method: Some(validation_method.clone()),
10654            domain_validation,
10655            options: AcmCertificateOptions {
10656                certificate_transparency_logging_preference: cert_transparency,
10657                export: "DISABLED".to_string(),
10658            },
10659            renewal_eligibility: "ELIGIBLE".to_string(),
10660            managed_by: None,
10661            certificate_authority_arn: ca_arn,
10662            tags,
10663            in_use_by: Vec::new(),
10664            describe_read_count: 0,
10665            failure_reason: None,
10666            renewal_summary,
10667        };
10668
10669        let mut accounts = self.acm_state.write();
10670        let account = accounts
10671            .accounts
10672            .entry(self.account_id.clone())
10673            .or_default();
10674        account.certificates.insert(arn.clone(), cert);
10675
10676        Ok(ProvisionResult::new(arn))
10677    }
10678
10679    fn delete_acm_certificate(&self, physical_id: &str) -> Result<(), String> {
10680        let mut accounts = self.acm_state.write();
10681        if let Some(account) = accounts.accounts.get_mut(&self.account_id) {
10682            account.certificates.remove(physical_id);
10683        }
10684        Ok(())
10685    }
10686
10687    /// Provision the singleton `AWS::CertificateManager::Account` resource —
10688    /// stores `ExpiryEventsConfiguration.DaysBeforeExpiry` on the account
10689    /// config so `GetAccountConfiguration` reflects it.
10690    fn create_acm_account(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
10691        let days = resource
10692            .properties
10693            .get("ExpiryEventsConfiguration")
10694            .and_then(|v| v.get("DaysBeforeExpiry"))
10695            .and_then(|v| v.as_i64())
10696            .map(|n| n as i32);
10697        let mut accounts = self.acm_state.write();
10698        let account = accounts
10699            .accounts
10700            .entry(self.account_id.clone())
10701            .or_default();
10702        account.account_config.expiry_events_days_before_expiry = days;
10703        Ok(ProvisionResult::new(format!(
10704            "acm-account-{}",
10705            self.account_id
10706        )))
10707    }
10708
10709    /// Reset the account config back to the default (no expiry events).
10710    /// AWS keeps the account around — the CFN deletion just clears the
10711    /// per-account override.
10712    fn delete_acm_account(&self) -> Result<(), String> {
10713        let mut accounts = self.acm_state.write();
10714        if let Some(account) = accounts.accounts.get_mut(&self.account_id) {
10715            account.account_config.expiry_events_days_before_expiry = None;
10716        }
10717        Ok(())
10718    }
10719
10720    // --- ElastiCache ---
10721
10722    fn create_ec_parameter_group(
10723        &self,
10724        resource: &ResourceDefinition,
10725    ) -> Result<ProvisionResult, String> {
10726        let props = &resource.properties;
10727        let name = props
10728            .get("CacheParameterGroupName")
10729            .and_then(|v| v.as_str())
10730            .unwrap_or(&resource.logical_id)
10731            .to_string();
10732        let family = props
10733            .get("CacheParameterGroupFamily")
10734            .and_then(|v| v.as_str())
10735            .unwrap_or("redis7")
10736            .to_string();
10737        let description = props
10738            .get("Description")
10739            .and_then(|v| v.as_str())
10740            .unwrap_or("")
10741            .to_string();
10742        let arn = format!(
10743            "arn:aws:elasticache:{}:{}:parametergroup:{}",
10744            self.region, self.account_id, name
10745        );
10746        let group = CacheParameterGroup {
10747            cache_parameter_group_name: name.clone(),
10748            cache_parameter_group_family: family,
10749            description,
10750            is_global: false,
10751            arn: arn.clone(),
10752        };
10753        let mut accounts = self.elasticache_state.write();
10754        let state = accounts.get_or_create(&self.account_id);
10755        // ParameterGroups stored as Vec — replace any existing entry.
10756        state
10757            .parameter_groups
10758            .retain(|p| p.cache_parameter_group_name != name);
10759        state.parameter_groups.push(group);
10760        Ok(ProvisionResult::new(name).with("Arn", arn))
10761    }
10762
10763    fn delete_ec_parameter_group(&self, physical_id: &str) -> Result<(), String> {
10764        let mut accounts = self.elasticache_state.write();
10765        let state = accounts.get_or_create(&self.account_id);
10766        state
10767            .parameter_groups
10768            .retain(|p| p.cache_parameter_group_name != physical_id);
10769        Ok(())
10770    }
10771
10772    fn create_ec_subnet_group(
10773        &self,
10774        resource: &ResourceDefinition,
10775    ) -> Result<ProvisionResult, String> {
10776        let props = &resource.properties;
10777        let name = props
10778            .get("CacheSubnetGroupName")
10779            .and_then(|v| v.as_str())
10780            .unwrap_or(&resource.logical_id)
10781            .to_string();
10782        let description = props
10783            .get("Description")
10784            .and_then(|v| v.as_str())
10785            .unwrap_or("")
10786            .to_string();
10787        let subnet_ids: Vec<String> = props
10788            .get("SubnetIds")
10789            .and_then(|v| v.as_array())
10790            .map(|arr| {
10791                arr.iter()
10792                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
10793                    .collect()
10794            })
10795            .unwrap_or_default();
10796        let arn = format!(
10797            "arn:aws:elasticache:{}:{}:subnetgroup:{}",
10798            self.region, self.account_id, name
10799        );
10800        let group = CacheSubnetGroup {
10801            cache_subnet_group_name: name.clone(),
10802            cache_subnet_group_description: description,
10803            vpc_id: String::new(),
10804            subnet_ids,
10805            arn: arn.clone(),
10806        };
10807        let mut accounts = self.elasticache_state.write();
10808        let state = accounts.get_or_create(&self.account_id);
10809        state.subnet_groups.insert(name.clone(), group);
10810        Ok(ProvisionResult::new(name).with("Arn", arn))
10811    }
10812
10813    fn delete_ec_subnet_group(&self, physical_id: &str) -> Result<(), String> {
10814        let mut accounts = self.elasticache_state.write();
10815        let state = accounts.get_or_create(&self.account_id);
10816        state.subnet_groups.remove(physical_id);
10817        Ok(())
10818    }
10819
10820    fn create_ec_security_group(
10821        &self,
10822        resource: &ResourceDefinition,
10823    ) -> Result<ProvisionResult, String> {
10824        let props = &resource.properties;
10825        let name = props
10826            .get("CacheSecurityGroupName")
10827            .and_then(|v| v.as_str())
10828            .unwrap_or(&resource.logical_id)
10829            .to_string();
10830        let description = props
10831            .get("Description")
10832            .and_then(|v| v.as_str())
10833            .unwrap_or("")
10834            .to_string();
10835        let arn = format!(
10836            "arn:aws:elasticache:{}:{}:securitygroup:{}",
10837            self.region, self.account_id, name
10838        );
10839        let group = CacheSecurityGroup {
10840            cache_security_group_name: name.clone(),
10841            description,
10842            owner_id: self.account_id.clone(),
10843            arn: arn.clone(),
10844            ec2_security_groups: Vec::new(),
10845        };
10846        let mut accounts = self.elasticache_state.write();
10847        let state = accounts.get_or_create(&self.account_id);
10848        state.security_groups.insert(name.clone(), group);
10849        Ok(ProvisionResult::new(name).with("Arn", arn))
10850    }
10851
10852    fn delete_ec_security_group(&self, physical_id: &str) -> Result<(), String> {
10853        let mut accounts = self.elasticache_state.write();
10854        let state = accounts.get_or_create(&self.account_id);
10855        state.security_groups.remove(physical_id);
10856        Ok(())
10857    }
10858
10859    fn create_ec_user(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
10860        let props = &resource.properties;
10861        let user_id = props
10862            .get("UserId")
10863            .and_then(|v| v.as_str())
10864            .unwrap_or(&resource.logical_id)
10865            .to_string();
10866        let user_name = props
10867            .get("UserName")
10868            .and_then(|v| v.as_str())
10869            .unwrap_or(&user_id)
10870            .to_string();
10871        let engine = props
10872            .get("Engine")
10873            .and_then(|v| v.as_str())
10874            .unwrap_or("redis")
10875            .to_string();
10876        let access_string = props
10877            .get("AccessString")
10878            .and_then(|v| v.as_str())
10879            .unwrap_or("on ~* +@all")
10880            .to_string();
10881        let authentication_type = props
10882            .get("AuthenticationMode")
10883            .and_then(|v| v.get("Type"))
10884            .and_then(|v| v.as_str())
10885            .unwrap_or("no-password-required")
10886            .to_string();
10887        let arn = format!(
10888            "arn:aws:elasticache:{}:{}:user:{}",
10889            self.region, self.account_id, user_id
10890        );
10891        let user = EcUser {
10892            user_id: user_id.clone(),
10893            user_name,
10894            engine,
10895            access_string,
10896            status: "active".to_string(),
10897            authentication_type,
10898            password_count: 0,
10899            arn: arn.clone(),
10900            minimum_engine_version: "6.0".to_string(),
10901            user_group_ids: Vec::new(),
10902        };
10903        let mut accounts = self.elasticache_state.write();
10904        let state = accounts.get_or_create(&self.account_id);
10905        state.users.insert(user_id.clone(), user);
10906        Ok(ProvisionResult::new(user_id).with("Arn", arn))
10907    }
10908
10909    fn delete_ec_user(&self, physical_id: &str) -> Result<(), String> {
10910        let mut accounts = self.elasticache_state.write();
10911        let state = accounts.get_or_create(&self.account_id);
10912        state.users.remove(physical_id);
10913        Ok(())
10914    }
10915
10916    fn create_ec_user_group(
10917        &self,
10918        resource: &ResourceDefinition,
10919    ) -> Result<ProvisionResult, String> {
10920        let props = &resource.properties;
10921        let user_group_id = props
10922            .get("UserGroupId")
10923            .and_then(|v| v.as_str())
10924            .unwrap_or(&resource.logical_id)
10925            .to_string();
10926        let engine = props
10927            .get("Engine")
10928            .and_then(|v| v.as_str())
10929            .unwrap_or("redis")
10930            .to_string();
10931        let user_ids: Vec<String> = props
10932            .get("UserIds")
10933            .and_then(|v| v.as_array())
10934            .map(|arr| {
10935                arr.iter()
10936                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
10937                    .collect()
10938            })
10939            .unwrap_or_default();
10940        let arn = format!(
10941            "arn:aws:elasticache:{}:{}:usergroup:{}",
10942            self.region, self.account_id, user_group_id
10943        );
10944        let group = EcUserGroup {
10945            user_group_id: user_group_id.clone(),
10946            engine,
10947            status: "active".to_string(),
10948            user_ids,
10949            arn: arn.clone(),
10950            minimum_engine_version: "6.0".to_string(),
10951            pending_changes: None,
10952            replication_groups: Vec::new(),
10953        };
10954        let mut accounts = self.elasticache_state.write();
10955        let state = accounts.get_or_create(&self.account_id);
10956        state.user_groups.insert(user_group_id.clone(), group);
10957        Ok(ProvisionResult::new(user_group_id).with("Arn", arn))
10958    }
10959
10960    fn delete_ec_user_group(&self, physical_id: &str) -> Result<(), String> {
10961        let mut accounts = self.elasticache_state.write();
10962        let state = accounts.get_or_create(&self.account_id);
10963        state.user_groups.remove(physical_id);
10964        Ok(())
10965    }
10966
10967    fn create_ec_cache_cluster(
10968        &self,
10969        resource: &ResourceDefinition,
10970    ) -> Result<ProvisionResult, String> {
10971        let props = &resource.properties;
10972        let id = props
10973            .get("ClusterName")
10974            .and_then(|v| v.as_str())
10975            .map(String::from)
10976            .unwrap_or_else(|| format!("cfn-cc-{}", resource.logical_id.to_lowercase()));
10977        let cache_node_type = props
10978            .get("CacheNodeType")
10979            .and_then(|v| v.as_str())
10980            .unwrap_or("cache.t4g.micro")
10981            .to_string();
10982        let engine = props
10983            .get("Engine")
10984            .and_then(|v| v.as_str())
10985            .unwrap_or("redis")
10986            .to_string();
10987        let engine_version = props
10988            .get("EngineVersion")
10989            .and_then(|v| v.as_str())
10990            .unwrap_or("7.1")
10991            .to_string();
10992        let num_cache_nodes = props
10993            .get("NumCacheNodes")
10994            .and_then(|v| v.as_i64())
10995            .map(|n| n as i32)
10996            .unwrap_or(1);
10997        let preferred_az = props
10998            .get("PreferredAvailabilityZone")
10999            .and_then(|v| v.as_str())
11000            .unwrap_or("us-east-1a")
11001            .to_string();
11002        let cache_subnet_group_name = props
11003            .get("CacheSubnetGroupName")
11004            .and_then(|v| v.as_str())
11005            .map(String::from);
11006        let auto_minor_version_upgrade = props
11007            .get("AutoMinorVersionUpgrade")
11008            .and_then(|v| v.as_bool())
11009            .unwrap_or(true);
11010        let default_port = if engine == "memcached" { 11211 } else { 6379 };
11011        let port = props
11012            .get("Port")
11013            .and_then(|v| v.as_i64())
11014            .map(|n| n as u16)
11015            .unwrap_or(default_port);
11016        let cache_parameter_group_name = props
11017            .get("CacheParameterGroupName")
11018            .and_then(|v| v.as_str())
11019            .map(String::from);
11020        let security_group_ids: Vec<String> = props
11021            .get("VpcSecurityGroupIds")
11022            .and_then(|v| v.as_array())
11023            .map(|arr| {
11024                arr.iter()
11025                    .filter_map(|v| v.as_str().map(String::from))
11026                    .collect()
11027            })
11028            .unwrap_or_default();
11029        let cache_security_group_names: Vec<String> = props
11030            .get("CacheSecurityGroupNames")
11031            .and_then(|v| v.as_array())
11032            .map(|arr| {
11033                arr.iter()
11034                    .filter_map(|v| v.as_str().map(String::from))
11035                    .collect()
11036            })
11037            .unwrap_or_default();
11038        let preferred_availability_zones: Vec<String> = props
11039            .get("PreferredAvailabilityZones")
11040            .and_then(|v| v.as_array())
11041            .map(|arr| {
11042                arr.iter()
11043                    .filter_map(|v| v.as_str().map(String::from))
11044                    .collect()
11045            })
11046            .unwrap_or_default();
11047        let snapshot_arns: Vec<String> = props
11048            .get("SnapshotArns")
11049            .and_then(|v| v.as_array())
11050            .map(|arr| {
11051                arr.iter()
11052                    .filter_map(|v| v.as_str().map(String::from))
11053                    .collect()
11054            })
11055            .unwrap_or_default();
11056        let snapshot_name = props
11057            .get("SnapshotName")
11058            .and_then(|v| v.as_str())
11059            .map(String::from);
11060        let snapshot_retention_limit = props
11061            .get("SnapshotRetentionLimit")
11062            .and_then(|v| v.as_i64())
11063            .map(|n| n as i32)
11064            .unwrap_or(0);
11065        let snapshot_window = props
11066            .get("SnapshotWindow")
11067            .and_then(|v| v.as_str())
11068            .map(String::from);
11069        let preferred_maintenance_window = props
11070            .get("PreferredMaintenanceWindow")
11071            .and_then(|v| v.as_str())
11072            .map(String::from);
11073        let notification_topic_arn = props
11074            .get("NotificationTopicArn")
11075            .and_then(|v| v.as_str())
11076            .map(String::from);
11077        let transit_encryption_enabled = props
11078            .get("TransitEncryptionEnabled")
11079            .and_then(|v| v.as_bool())
11080            .unwrap_or(false);
11081        let auth_token = props
11082            .get("AuthToken")
11083            .and_then(|v| v.as_str())
11084            .filter(|s| !s.is_empty())
11085            .map(String::from);
11086        let auth_token_enabled = auth_token.is_some();
11087        let network_type = props
11088            .get("NetworkType")
11089            .and_then(|v| v.as_str())
11090            .map(String::from)
11091            .or_else(|| Some("ipv4".to_string()));
11092        let ip_discovery = props
11093            .get("IpDiscovery")
11094            .and_then(|v| v.as_str())
11095            .map(String::from)
11096            .or_else(|| Some("ipv4".to_string()));
11097        let az_mode = props
11098            .get("AZMode")
11099            .and_then(|v| v.as_str())
11100            .map(String::from)
11101            .or_else(|| Some("single-az".to_string()));
11102        let outpost_mode = props
11103            .get("OutpostMode")
11104            .and_then(|v| v.as_str())
11105            .map(String::from);
11106        let preferred_outpost_arn = props
11107            .get("PreferredOutpostArn")
11108            .and_then(|v| v.as_str())
11109            .map(String::from);
11110
11111        let mut accounts = self.elasticache_state.write();
11112        let state = accounts.get_or_create(&self.account_id);
11113        let arn = format!(
11114            "arn:aws:elasticache:{}:{}:cluster:{}",
11115            state.region, state.account_id, id
11116        );
11117        let endpoint_address = format!("{id}.fakecloud.{}.cache.amazonaws.com", state.region);
11118        let cluster = EcCacheCluster {
11119            cache_cluster_id: id.clone(),
11120            cache_node_type,
11121            engine,
11122            engine_version,
11123            cache_cluster_status: "available".to_string(),
11124            num_cache_nodes,
11125            preferred_availability_zone: preferred_az,
11126            cache_subnet_group_name,
11127            auto_minor_version_upgrade,
11128            arn: arn.clone(),
11129            created_at: Utc::now().to_rfc3339(),
11130            endpoint_address: endpoint_address.clone(),
11131            endpoint_port: port,
11132            container_id: String::new(),
11133            host_port: 0,
11134            replication_group_id: None,
11135            cache_parameter_group_name,
11136            security_group_ids,
11137            log_delivery_configurations: Vec::new(),
11138            transit_encryption_enabled,
11139            at_rest_encryption_enabled: false,
11140            auth_token_enabled,
11141            port,
11142            preferred_maintenance_window,
11143            preferred_availability_zones,
11144            notification_topic_arn,
11145            cache_security_group_names,
11146            snapshot_arns,
11147            snapshot_name,
11148            snapshot_retention_limit,
11149            snapshot_window,
11150            outpost_mode,
11151            preferred_outpost_arn,
11152            network_type,
11153            ip_discovery,
11154            az_mode,
11155            auth_token,
11156            kms_key_id: None,
11157            transit_encryption_mode: None,
11158            data_tiering_enabled: None,
11159            cluster_mode: None,
11160            preferred_outpost_arns: Vec::new(),
11161        };
11162        state.cache_clusters.insert(id.clone(), cluster);
11163        Ok(ProvisionResult::new(id.clone())
11164            .with("Arn", arn)
11165            .with("RedisEndpoint.Address", endpoint_address.clone())
11166            .with("RedisEndpoint.Port", port.to_string())
11167            .with("ConfigurationEndpoint.Address", endpoint_address)
11168            .with("ConfigurationEndpoint.Port", port.to_string()))
11169    }
11170
11171    fn delete_ec_cache_cluster(&self, physical_id: &str) -> Result<(), String> {
11172        let mut accounts = self.elasticache_state.write();
11173        let state = accounts.get_or_create(&self.account_id);
11174        state.cache_clusters.remove(physical_id);
11175        Ok(())
11176    }
11177
11178    fn create_ec_replication_group(
11179        &self,
11180        resource: &ResourceDefinition,
11181    ) -> Result<ProvisionResult, String> {
11182        let props = &resource.properties;
11183        let id = props
11184            .get("ReplicationGroupId")
11185            .and_then(|v| v.as_str())
11186            .map(String::from)
11187            .unwrap_or_else(|| format!("cfn-rg-{}", resource.logical_id.to_lowercase()));
11188        let description = props
11189            .get("ReplicationGroupDescription")
11190            .and_then(|v| v.as_str())
11191            .unwrap_or("CFN-provisioned replication group")
11192            .to_string();
11193        let cache_node_type = props
11194            .get("CacheNodeType")
11195            .and_then(|v| v.as_str())
11196            .unwrap_or("cache.t4g.micro")
11197            .to_string();
11198        let engine = props
11199            .get("Engine")
11200            .and_then(|v| v.as_str())
11201            .unwrap_or("redis")
11202            .to_string();
11203        let engine_version = props
11204            .get("EngineVersion")
11205            .and_then(|v| v.as_str())
11206            .unwrap_or("7.1")
11207            .to_string();
11208        let num_cache_clusters = props
11209            .get("NumCacheClusters")
11210            .and_then(|v| v.as_i64())
11211            .map(|n| n as i32)
11212            .unwrap_or(1);
11213        let num_node_groups = props
11214            .get("NumNodeGroups")
11215            .and_then(|v| v.as_i64())
11216            .map(|n| n as i32)
11217            .unwrap_or(0);
11218        let replicas_per_node_group = props
11219            .get("ReplicasPerNodeGroup")
11220            .and_then(|v| v.as_i64())
11221            .map(|n| n as i32);
11222        let automatic_failover_enabled = props
11223            .get("AutomaticFailoverEnabled")
11224            .and_then(|v| v.as_bool())
11225            .unwrap_or(false);
11226        let multi_az_enabled = props
11227            .get("MultiAZEnabled")
11228            .and_then(|v| v.as_bool())
11229            .unwrap_or(false);
11230        let transit_encryption_enabled = props
11231            .get("TransitEncryptionEnabled")
11232            .and_then(|v| v.as_bool())
11233            .unwrap_or(false);
11234        let at_rest_encryption_enabled = props
11235            .get("AtRestEncryptionEnabled")
11236            .and_then(|v| v.as_bool())
11237            .unwrap_or(false);
11238        let kms_key_id = props
11239            .get("KmsKeyId")
11240            .and_then(|v| v.as_str())
11241            .map(String::from);
11242        let auth_token_enabled = props
11243            .get("AuthToken")
11244            .and_then(|v| v.as_str())
11245            .filter(|s| !s.is_empty())
11246            .is_some();
11247        let user_group_ids: Vec<String> = props
11248            .get("UserGroupIds")
11249            .and_then(|v| v.as_array())
11250            .map(|arr| {
11251                arr.iter()
11252                    .filter_map(|v| v.as_str().map(String::from))
11253                    .collect()
11254            })
11255            .unwrap_or_default();
11256        let snapshot_retention_limit = props
11257            .get("SnapshotRetentionLimit")
11258            .and_then(|v| v.as_i64())
11259            .map(|n| n as i32)
11260            .unwrap_or(0);
11261        let snapshot_window = props
11262            .get("SnapshotWindow")
11263            .and_then(|v| v.as_str())
11264            .unwrap_or("00:00-01:00")
11265            .to_string();
11266        let port = props
11267            .get("Port")
11268            .and_then(|v| v.as_i64())
11269            .map(|n| n as u16)
11270            .unwrap_or(6379);
11271        let cluster_enabled = num_node_groups > 1
11272            || props
11273                .get("ClusterEnabled")
11274                .and_then(|v| v.as_bool())
11275                .unwrap_or(false);
11276
11277        let mut accounts = self.elasticache_state.write();
11278        let state = accounts.get_or_create(&self.account_id);
11279        let arn = format!(
11280            "arn:aws:elasticache:{}:{}:replicationgroup:{}",
11281            state.region, state.account_id, id
11282        );
11283        let endpoint_address = format!(
11284            "{id}.fakecloud.ng.0001.{}.cache.amazonaws.com",
11285            state.region
11286        );
11287        let configuration_endpoint = if cluster_enabled {
11288            Some(format!(
11289                "{id}.fakecloud.cfg.{}.cache.amazonaws.com",
11290                state.region
11291            ))
11292        } else {
11293            None
11294        };
11295
11296        let group = EcReplicationGroup {
11297            replication_group_id: id.clone(),
11298            description,
11299            global_replication_group_id: None,
11300            global_replication_group_role: None,
11301            status: "available".to_string(),
11302            cache_node_type,
11303            engine,
11304            engine_version,
11305            num_cache_clusters,
11306            automatic_failover_enabled,
11307            endpoint_address: endpoint_address.clone(),
11308            endpoint_port: port,
11309            arn: arn.clone(),
11310            created_at: Utc::now().to_rfc3339(),
11311            container_id: String::new(),
11312            host_port: 0,
11313            member_clusters: Vec::new(),
11314            snapshot_retention_limit,
11315            snapshot_window,
11316            transit_encryption_enabled,
11317            at_rest_encryption_enabled,
11318            cluster_enabled,
11319            kms_key_id,
11320            auth_token_enabled,
11321            user_group_ids,
11322            multi_az_enabled,
11323            log_delivery_configurations: Vec::new(),
11324            data_tiering: props
11325                .get("DataTieringEnabled")
11326                .and_then(|v| v.as_bool())
11327                .map(|b| if b { "enabled" } else { "disabled" }.to_string()),
11328            ip_discovery: props
11329                .get("IpDiscovery")
11330                .and_then(|v| v.as_str())
11331                .map(String::from),
11332            network_type: props
11333                .get("NetworkType")
11334                .and_then(|v| v.as_str())
11335                .map(String::from),
11336            transit_encryption_mode: props
11337                .get("TransitEncryptionMode")
11338                .and_then(|v| v.as_str())
11339                .map(String::from),
11340            num_node_groups,
11341            configuration_endpoint_address: configuration_endpoint.clone(),
11342            configuration_endpoint_port: configuration_endpoint.as_ref().map(|_| port),
11343            replicas_per_node_group,
11344            auth_token: props
11345                .get("AuthToken")
11346                .and_then(|v| v.as_str())
11347                .filter(|s| !s.is_empty())
11348                .map(String::from),
11349            port,
11350            notification_topic_arn: props
11351                .get("NotificationTopicArn")
11352                .and_then(|v| v.as_str())
11353                .map(String::from),
11354            cluster_mode: props
11355                .get("ClusterMode")
11356                .and_then(|v| v.as_str())
11357                .map(String::from),
11358            data_tiering_enabled: props.get("DataTieringEnabled").and_then(|v| v.as_bool()),
11359            notification_topic_status: None,
11360            cache_parameter_group_name: props
11361                .get("CacheParameterGroupName")
11362                .and_then(|v| v.as_str())
11363                .map(String::from),
11364            cache_subnet_group_name: props
11365                .get("CacheSubnetGroupName")
11366                .and_then(|v| v.as_str())
11367                .map(String::from),
11368            security_group_ids: props
11369                .get("SecurityGroupIds")
11370                .and_then(|v| v.as_array())
11371                .map(|arr| {
11372                    arr.iter()
11373                        .filter_map(|v| v.as_str().map(String::from))
11374                        .collect()
11375                })
11376                .unwrap_or_default(),
11377            preferred_maintenance_window: props
11378                .get("PreferredMaintenanceWindow")
11379                .and_then(|v| v.as_str())
11380                .map(String::from),
11381            snapshot_name: props
11382                .get("SnapshotName")
11383                .and_then(|v| v.as_str())
11384                .map(String::from),
11385            snapshot_arns: props
11386                .get("SnapshotArns")
11387                .and_then(|v| v.as_array())
11388                .map(|arr| {
11389                    arr.iter()
11390                        .filter_map(|v| v.as_str().map(String::from))
11391                        .collect()
11392                })
11393                .unwrap_or_default(),
11394            auto_minor_version_upgrade: props
11395                .get("AutoMinorVersionUpgrade")
11396                .and_then(|v| v.as_bool())
11397                .unwrap_or(true),
11398        };
11399        state.replication_groups.insert(id.clone(), group);
11400
11401        let mut result = ProvisionResult::new(id.clone())
11402            .with("Arn", arn)
11403            .with("PrimaryEndPoint.Address", endpoint_address.clone())
11404            .with("PrimaryEndPoint.Port", port.to_string())
11405            .with("ReadEndPoint.Addresses", endpoint_address.clone())
11406            .with("ReadEndPoint.Ports", port.to_string());
11407        if let Some(cfg) = configuration_endpoint {
11408            result = result
11409                .with("ConfigurationEndPoint.Address", cfg)
11410                .with("ConfigurationEndPoint.Port", port.to_string());
11411        }
11412        Ok(result)
11413    }
11414
11415    fn delete_ec_replication_group(&self, physical_id: &str) -> Result<(), String> {
11416        let mut accounts = self.elasticache_state.write();
11417        let state = accounts.get_or_create(&self.account_id);
11418        state.replication_groups.remove(physical_id);
11419        Ok(())
11420    }
11421
11422    // --- Route53 ---
11423
11424    fn create_route53_hosted_zone(
11425        &self,
11426        resource: &ResourceDefinition,
11427    ) -> Result<ProvisionResult, String> {
11428        let props = &resource.properties;
11429        let name = props
11430            .get("Name")
11431            .and_then(|v| v.as_str())
11432            .ok_or("Name is required")?
11433            .to_string();
11434        let normalized_name = if name.ends_with('.') {
11435            name.clone()
11436        } else {
11437            format!("{name}.")
11438        };
11439        let comment = props
11440            .get("HostedZoneConfig")
11441            .and_then(|v| v.get("Comment"))
11442            .and_then(|v| v.as_str())
11443            .map(|s| s.to_string());
11444        let private_zone = props
11445            .get("VPCs")
11446            .and_then(|v| v.as_array())
11447            .map(|a| !a.is_empty())
11448            .unwrap_or(false);
11449        let vpcs: Vec<fakecloud_route53::model::VPC> = props
11450            .get("VPCs")
11451            .and_then(|v| v.as_array())
11452            .map(|arr| {
11453                arr.iter()
11454                    .map(|vpc| fakecloud_route53::model::VPC {
11455                        vpc_id: vpc.get("VPCId").and_then(|v| v.as_str()).map(String::from),
11456                        vpc_region: vpc
11457                            .get("VPCRegion")
11458                            .and_then(|v| v.as_str())
11459                            .map(String::from),
11460                    })
11461                    .collect()
11462            })
11463            .unwrap_or_default();
11464
11465        let id = format!(
11466            "Z{}",
11467            Uuid::new_v4().simple().to_string()[..14].to_uppercase()
11468        );
11469        let name_servers = (1..=4)
11470            .map(|i| format!("ns-{}.awsdns-{:02}.com", 100 + i, i))
11471            .collect::<Vec<_>>();
11472
11473        let zone = StoredHostedZone {
11474            id: id.clone(),
11475            name: normalized_name,
11476            caller_reference: format!("cfn-{}", resource.logical_id),
11477            comment,
11478            private_zone,
11479            features: Some(HostedZoneFeatures::default()),
11480            vpcs,
11481            delegation_set_id: None,
11482            name_servers: name_servers.clone(),
11483            created_time: Utc::now(),
11484            resource_record_sets: Vec::new(),
11485        };
11486
11487        let mut accounts = self.route53_state.write();
11488        // Route53 is a global service in fakecloud; all entries share the
11489        // default account bucket so SDK reads land on the same data.
11490        let state = accounts.entry("000000000000");
11491        state.hosted_zones.insert(id.clone(), zone);
11492
11493        let mut result = ProvisionResult::new(id.clone()).with("Id", id);
11494        for (i, ns) in name_servers.iter().enumerate() {
11495            result = result.with(&format!("NameServers.{i}"), ns.clone());
11496        }
11497        result = result.with("NameServers", name_servers.join(","));
11498        Ok(result)
11499    }
11500
11501    fn delete_route53_hosted_zone(&self, physical_id: &str) -> Result<(), String> {
11502        let mut accounts = self.route53_state.write();
11503        // Route53 is a global service in fakecloud; all entries share the
11504        // default account bucket so SDK reads land on the same data.
11505        let state = accounts.entry("000000000000");
11506        state.hosted_zones.remove(physical_id);
11507        Ok(())
11508    }
11509
11510    fn create_route53_record_set(
11511        &self,
11512        resource: &ResourceDefinition,
11513    ) -> Result<ProvisionResult, String> {
11514        let props = &resource.properties;
11515        let zone_id = props
11516            .get("HostedZoneId")
11517            .and_then(|v| v.as_str())
11518            .ok_or_else(|| {
11519                "HostedZoneId is required (HostedZoneName lookups not supported)".to_string()
11520            })?
11521            .to_string();
11522        let name = props
11523            .get("Name")
11524            .and_then(|v| v.as_str())
11525            .ok_or("Name is required")?
11526            .to_string();
11527        let normalized_name = if name.ends_with('.') {
11528            name.clone()
11529        } else {
11530            format!("{name}.")
11531        };
11532        let record_type = props
11533            .get("Type")
11534            .and_then(|v| v.as_str())
11535            .ok_or("Type is required")?
11536            .to_string();
11537        let ttl = props.get("TTL").and_then(|v| {
11538            v.as_str()
11539                .and_then(|s| s.parse::<i64>().ok())
11540                .or_else(|| v.as_i64())
11541        });
11542        let resource_records = props
11543            .get("ResourceRecords")
11544            .and_then(|v| v.as_array())
11545            .map(|arr| {
11546                let recs: Vec<fakecloud_route53::model::ResourceRecord> = arr
11547                    .iter()
11548                    .filter_map(|v| {
11549                        v.as_str()
11550                            .map(|s| fakecloud_route53::model::ResourceRecord {
11551                                value: s.to_string(),
11552                            })
11553                    })
11554                    .collect();
11555                fakecloud_route53::model::ResourceRecords {
11556                    resource_record: recs,
11557                }
11558            });
11559        let alias_target =
11560            props
11561                .get("AliasTarget")
11562                .map(|v| fakecloud_route53::model::AliasTarget {
11563                    hosted_zone_id: v
11564                        .get("HostedZoneId")
11565                        .and_then(|x| x.as_str())
11566                        .unwrap_or("")
11567                        .to_string(),
11568                    dns_name: v
11569                        .get("DNSName")
11570                        .and_then(|x| x.as_str())
11571                        .unwrap_or("")
11572                        .to_string(),
11573                    evaluate_target_health: v
11574                        .get("EvaluateTargetHealth")
11575                        .and_then(|x| x.as_bool())
11576                        .unwrap_or(false),
11577                });
11578        let set_identifier = props
11579            .get("SetIdentifier")
11580            .and_then(|v| v.as_str())
11581            .map(String::from);
11582        let weight = props.get("Weight").and_then(|v| {
11583            v.as_i64()
11584                .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11585        });
11586        let region = props
11587            .get("Region")
11588            .and_then(|v| v.as_str())
11589            .map(String::from);
11590        let failover = props
11591            .get("Failover")
11592            .and_then(|v| v.as_str())
11593            .map(String::from);
11594        let multi_value_answer = props.get("MultiValueAnswer").and_then(|v| v.as_bool());
11595        let health_check_id = props
11596            .get("HealthCheckId")
11597            .and_then(|v| v.as_str())
11598            .map(String::from);
11599
11600        let rrset = ResourceRecordSet {
11601            name: normalized_name.clone(),
11602            record_type: record_type.clone(),
11603            set_identifier: set_identifier.clone(),
11604            weight,
11605            region,
11606            geo_location: None,
11607            failover,
11608            multi_value_answer,
11609            ttl,
11610            resource_records,
11611            alias_target,
11612            health_check_id,
11613            traffic_policy_instance_id: None,
11614            cidr_routing_config: None,
11615            geo_proximity_location: None,
11616        };
11617
11618        let mut accounts = self.route53_state.write();
11619        // Route53 is a global service in fakecloud; all entries share the
11620        // default account bucket so SDK reads land on the same data.
11621        let state = accounts.entry("000000000000");
11622        let zone = state.hosted_zones.get_mut(&zone_id).ok_or_else(|| {
11623            format!(
11624                "HostedZone {zone_id} not yet provisioned; will retry once it has been provisioned"
11625            )
11626        })?;
11627        // Replace existing record with matching (name, type, set_identifier).
11628        zone.resource_record_sets.retain(|r| {
11629            !(r.name == rrset.name
11630                && r.record_type == rrset.record_type
11631                && r.set_identifier == rrset.set_identifier)
11632        });
11633        zone.resource_record_sets.push(rrset);
11634
11635        let physical_id = match &set_identifier {
11636            Some(sid) => format!("{zone_id}|{normalized_name}|{record_type}|{sid}"),
11637            None => format!("{zone_id}|{normalized_name}|{record_type}"),
11638        };
11639        Ok(ProvisionResult::new(physical_id))
11640    }
11641
11642    fn delete_route53_record_set(
11643        &self,
11644        physical_id: &str,
11645        _attributes: &BTreeMap<String, String>,
11646    ) -> Result<(), String> {
11647        let parts: Vec<&str> = physical_id.split('|').collect();
11648        if parts.len() < 3 {
11649            return Ok(());
11650        }
11651        let zone_id = parts[0];
11652        let name = parts[1];
11653        let record_type = parts[2];
11654        let set_identifier = parts.get(3).map(|s| s.to_string());
11655
11656        let mut accounts = self.route53_state.write();
11657        // Route53 is a global service in fakecloud; all entries share the
11658        // default account bucket so SDK reads land on the same data.
11659        let state = accounts.entry("000000000000");
11660        if let Some(zone) = state.hosted_zones.get_mut(zone_id) {
11661            zone.resource_record_sets.retain(|r| {
11662                !(r.name == name
11663                    && r.record_type == record_type
11664                    && r.set_identifier == set_identifier)
11665            });
11666        }
11667        Ok(())
11668    }
11669
11670    fn create_route53_health_check(
11671        &self,
11672        resource: &ResourceDefinition,
11673    ) -> Result<ProvisionResult, String> {
11674        let props = &resource.properties;
11675        let cfg_value = props
11676            .get("HealthCheckConfig")
11677            .ok_or("HealthCheckConfig is required")?;
11678
11679        let health_check_type = cfg_value
11680            .get("Type")
11681            .and_then(|v| v.as_str())
11682            .ok_or("HealthCheckConfig.Type is required")?
11683            .to_string();
11684
11685        let cfg = HealthCheckConfig {
11686            ip_address: cfg_value
11687                .get("IPAddress")
11688                .and_then(|v| v.as_str())
11689                .map(String::from),
11690            port: cfg_value
11691                .get("Port")
11692                .and_then(|v| {
11693                    v.as_i64()
11694                        .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11695                })
11696                .map(|n| n as i32),
11697            health_check_type,
11698            resource_path: cfg_value
11699                .get("ResourcePath")
11700                .and_then(|v| v.as_str())
11701                .map(String::from),
11702            fully_qualified_domain_name: cfg_value
11703                .get("FullyQualifiedDomainName")
11704                .and_then(|v| v.as_str())
11705                .map(String::from),
11706            search_string: cfg_value
11707                .get("SearchString")
11708                .and_then(|v| v.as_str())
11709                .map(String::from),
11710            request_interval: cfg_value
11711                .get("RequestInterval")
11712                .and_then(|v| {
11713                    v.as_i64()
11714                        .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11715                })
11716                .map(|n| n as i32),
11717            failure_threshold: cfg_value
11718                .get("FailureThreshold")
11719                .and_then(|v| {
11720                    v.as_i64()
11721                        .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11722                })
11723                .map(|n| n as i32),
11724            measure_latency: cfg_value.get("MeasureLatency").and_then(|v| v.as_bool()),
11725            inverted: cfg_value.get("Inverted").and_then(|v| v.as_bool()),
11726            disabled: cfg_value.get("Disabled").and_then(|v| v.as_bool()),
11727            health_threshold: cfg_value
11728                .get("HealthThreshold")
11729                .and_then(|v| {
11730                    v.as_i64()
11731                        .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
11732                })
11733                .map(|n| n as i32),
11734            child_health_checks: cfg_value
11735                .get("ChildHealthChecks")
11736                .and_then(|v| v.as_array())
11737                .map(|arr| fakecloud_route53::model::ChildHealthChecks {
11738                    child_health_check: arr
11739                        .iter()
11740                        .filter_map(|v| v.as_str().map(String::from))
11741                        .collect(),
11742                }),
11743            enable_sni: cfg_value.get("EnableSNI").and_then(|v| v.as_bool()),
11744            regions: cfg_value
11745                .get("Regions")
11746                .and_then(|v| v.as_array())
11747                .map(|arr| fakecloud_route53::model::HealthCheckRegions {
11748                    region: arr
11749                        .iter()
11750                        .filter_map(|v| v.as_str().map(String::from))
11751                        .collect(),
11752                }),
11753            alarm_identifier: cfg_value.get("AlarmIdentifier").map(|v| {
11754                fakecloud_route53::model::AlarmIdentifier {
11755                    region: v
11756                        .get("Region")
11757                        .and_then(|x| x.as_str())
11758                        .unwrap_or("")
11759                        .to_string(),
11760                    name: v
11761                        .get("Name")
11762                        .and_then(|x| x.as_str())
11763                        .unwrap_or("")
11764                        .to_string(),
11765                }
11766            }),
11767            insufficient_data_health_status: cfg_value
11768                .get("InsufficientDataHealthStatus")
11769                .and_then(|v| v.as_str())
11770                .map(String::from),
11771            routing_control_arn: cfg_value
11772                .get("RoutingControlArn")
11773                .and_then(|v| v.as_str())
11774                .map(String::from),
11775        };
11776
11777        let id = Uuid::new_v4().to_string();
11778        let hc = StoredHealthCheck {
11779            id: id.clone(),
11780            caller_reference: format!("cfn-{}", resource.logical_id),
11781            version: 1,
11782            config: cfg,
11783            created_time: Utc::now(),
11784            status: fakecloud_route53::HealthCheckStatus::Success,
11785            last_failure_reason: None,
11786        };
11787
11788        let mut accounts = self.route53_state.write();
11789        // Route53 is a global service in fakecloud; all entries share the
11790        // default account bucket so SDK reads land on the same data.
11791        let state = accounts.entry("000000000000");
11792        state.health_checks.insert(id.clone(), hc);
11793
11794        Ok(ProvisionResult::new(id.clone()).with("HealthCheckId", id))
11795    }
11796
11797    fn delete_route53_health_check(&self, physical_id: &str) -> Result<(), String> {
11798        let mut accounts = self.route53_state.write();
11799        // Route53 is a global service in fakecloud; all entries share the
11800        // default account bucket so SDK reads land on the same data.
11801        let state = accounts.entry("000000000000");
11802        state.health_checks.remove(physical_id);
11803        Ok(())
11804    }
11805
11806    /// `AWS::Route53::DNSSEC` flips the hosted zone's `dnssec_status` to
11807    /// `SIGNING`. Physical id is the hosted zone id so the delete path can
11808    /// flip it back without consulting the template.
11809    fn create_route53_dnssec(
11810        &self,
11811        resource: &ResourceDefinition,
11812    ) -> Result<ProvisionResult, String> {
11813        let zone_id = resource
11814            .properties
11815            .get("HostedZoneId")
11816            .and_then(|v| v.as_str())
11817            .ok_or_else(|| "HostedZoneId is required".to_string())?
11818            .trim_start_matches("/hostedzone/")
11819            .to_string();
11820        let mut accounts = self.route53_state.write();
11821        let state = accounts.entry("000000000000");
11822        if !state.hosted_zones.contains_key(&zone_id) {
11823            return Err(format!("HostedZone {zone_id} does not exist"));
11824        }
11825        state
11826            .dnssec_status
11827            .insert(zone_id.clone(), "SIGNING".to_string());
11828        Ok(ProvisionResult::new(zone_id))
11829    }
11830
11831    fn delete_route53_dnssec(&self, physical_id: &str) -> Result<(), String> {
11832        let mut accounts = self.route53_state.write();
11833        let state = accounts.entry("000000000000");
11834        state.dnssec_status.remove(physical_id);
11835        Ok(())
11836    }
11837
11838    /// `AWS::Route53::KeySigningKey` registers a KSK against a hosted
11839    /// zone. Physical id encodes `<hosted_zone_id>/<name>` so the delete
11840    /// path can find the (zone, name) tuple without re-reading inputs.
11841    fn create_route53_key_signing_key(
11842        &self,
11843        resource: &ResourceDefinition,
11844    ) -> Result<ProvisionResult, String> {
11845        let props = &resource.properties;
11846        let zone_id = props
11847            .get("HostedZoneId")
11848            .and_then(|v| v.as_str())
11849            .ok_or_else(|| "HostedZoneId is required".to_string())?
11850            .trim_start_matches("/hostedzone/")
11851            .to_string();
11852        let name = props
11853            .get("Name")
11854            .and_then(|v| v.as_str())
11855            .ok_or_else(|| "Name is required".to_string())?
11856            .to_string();
11857        let kms_arn = props
11858            .get("KeyManagementServiceArn")
11859            .and_then(|v| v.as_str())
11860            .ok_or_else(|| "KeyManagementServiceArn is required".to_string())?
11861            .to_string();
11862        let status = props
11863            .get("Status")
11864            .and_then(|v| v.as_str())
11865            .unwrap_or("ACTIVE")
11866            .to_string();
11867        let now = Utc::now();
11868        // Derive the same deterministic ECDSA P-256 KSK material the
11869        // direct CreateKeySigningKey path produces so DNSSEC features
11870        // (DS digest, RRSIG signing, the
11871        // `/_fakecloud/route53/zones/{id}/dnssec` admin endpoint)
11872        // round-trip identically through CloudFormation.
11873        let key_material = fakecloud_route53::dnssec::derive_keypair(&zone_id, &name);
11874        let key_tag = fakecloud_route53::dnssec::key_tag_for(&key_material.dnskey_public_key);
11875        let mut accounts = self.route53_state.write();
11876        let state = accounts.entry("000000000000");
11877        if !state.hosted_zones.contains_key(&zone_id) {
11878            return Err(format!("HostedZone {zone_id} does not exist"));
11879        }
11880        let zone_name = state
11881            .hosted_zones
11882            .get(&zone_id)
11883            .map(|z| z.name.clone())
11884            .unwrap_or_else(|| ".".to_string());
11885        let ds_digest_hex = fakecloud_route53::dnssec::ds_digest_sha256(
11886            &zone_name,
11887            key_tag,
11888            &key_material.dnskey_public_key,
11889        );
11890        let ksk = fakecloud_route53::StoredKeySigningKey {
11891            hosted_zone_id: zone_id.clone(),
11892            name: name.clone(),
11893            kms_arn,
11894            status,
11895            caller_reference: format!("cfn-{}", Uuid::new_v4()),
11896            created_date: now,
11897            last_modified_date: now,
11898            key_tag: key_tag as i32,
11899            private_key_pem: key_material.private_key_pem,
11900            public_key_der: key_material.public_key_der,
11901            ds_digest_hex,
11902        };
11903        state
11904            .key_signing_keys
11905            .insert((zone_id.clone(), name.clone()), ksk);
11906        Ok(ProvisionResult::new(format!("{zone_id}/{name}")))
11907    }
11908
11909    fn delete_route53_key_signing_key(&self, physical_id: &str) -> Result<(), String> {
11910        let (zone_id, name) = match physical_id.split_once('/') {
11911            Some(parts) => parts,
11912            None => return Ok(()),
11913        };
11914        let mut accounts = self.route53_state.write();
11915        let state = accounts.entry("000000000000");
11916        state
11917            .key_signing_keys
11918            .remove(&(zone_id.to_string(), name.to_string()));
11919        Ok(())
11920    }
11921
11922    // --- CloudFront ---
11923    //
11924    // CloudFront is a global service that stores data under the default
11925    // account bucket; mirror that here so SDK reads land on the same data
11926    // CFN wrote.
11927
11928    fn create_cf_origin_access_identity(
11929        &self,
11930        resource: &ResourceDefinition,
11931    ) -> Result<ProvisionResult, String> {
11932        let props = &resource.properties;
11933        let cfg = props
11934            .get("CloudFrontOriginAccessIdentityConfig")
11935            .ok_or("CloudFrontOriginAccessIdentityConfig is required")?;
11936        let comment = cfg
11937            .get("Comment")
11938            .and_then(|v| v.as_str())
11939            .unwrap_or("")
11940            .to_string();
11941        let caller_reference = format!("cfn-{}", resource.logical_id);
11942
11943        let id = format!(
11944            "E{}",
11945            Uuid::new_v4().simple().to_string()[..13].to_uppercase()
11946        );
11947        let etag = format!(
11948            "E{}",
11949            Uuid::new_v4().simple().to_string()[..7].to_uppercase()
11950        );
11951        let s3_canonical_user_id = format!(
11952            "{:0<64}",
11953            Uuid::new_v4().simple().to_string().to_lowercase()
11954        );
11955
11956        let oai = StoredOriginAccessIdentity {
11957            id: id.clone(),
11958            etag,
11959            s3_canonical_user_id: s3_canonical_user_id.clone(),
11960            config: CloudFrontOriginAccessIdentityConfig {
11961                caller_reference,
11962                comment,
11963            },
11964        };
11965
11966        let mut accounts = self.cloudfront_state.write();
11967        let state = accounts.entry("000000000000");
11968        state.origin_access_identities.insert(id.clone(), oai);
11969
11970        Ok(ProvisionResult::new(id.clone())
11971            .with("Id", id)
11972            .with("S3CanonicalUserId", s3_canonical_user_id))
11973    }
11974
11975    fn delete_cf_origin_access_identity(&self, physical_id: &str) -> Result<(), String> {
11976        let mut accounts = self.cloudfront_state.write();
11977        let state = accounts.entry("000000000000");
11978        state.origin_access_identities.remove(physical_id);
11979        Ok(())
11980    }
11981
11982    /// Provision an `AWS::CloudFront::Distribution`. Reads
11983    /// DistributionConfig.Origins/DefaultCacheBehavior/etc. and persists
11984    /// a StoredDistribution in CloudFront state. CFN's Origins property
11985    /// is a flat array, so we wrap it back into the wire shape with a
11986    /// quantity + Items.Origin nesting.
11987    fn create_cf_distribution(
11988        &self,
11989        resource: &ResourceDefinition,
11990    ) -> Result<ProvisionResult, String> {
11991        let cfg = resource
11992            .properties
11993            .get("DistributionConfig")
11994            .ok_or_else(|| "DistributionConfig is required".to_string())?;
11995
11996        // CFN Origins is a flat JSON array; the wire shape is
11997        // { Quantity, Items: { Origin: [...] } }. Translate. CFN's
11998        // PascalCase doesn't always match the wire model — Origin's
11999        // CustomOriginConfig uses HTTPPort/HTTPSPort while the model
12000        // expects HttpPort/HttpsPort, so we patch a few well-known
12001        // renames before letting serde finish the parse.
12002        let origin_entries: Vec<Origin> = cfg
12003            .get("Origins")
12004            .and_then(|v| v.as_array())
12005            .ok_or_else(|| "DistributionConfig.Origins is required".to_string())?
12006            .iter()
12007            .map(|o| {
12008                let mut patched = o.clone();
12009                if let Some(custom) = patched
12010                    .get_mut("CustomOriginConfig")
12011                    .and_then(|v| v.as_object_mut())
12012                {
12013                    if let Some(v) = custom.remove("HTTPPort") {
12014                        custom.insert("HttpPort".to_string(), v);
12015                    }
12016                    if let Some(v) = custom.remove("HTTPSPort") {
12017                        custom.insert("HttpsPort".to_string(), v);
12018                    }
12019                }
12020                serde_json::from_value::<Origin>(patched)
12021                    .map_err(|e| format!("Invalid Origin entry: {e}"))
12022            })
12023            .collect::<Result<Vec<_>, _>>()?;
12024        if origin_entries.is_empty() {
12025            return Err("DistributionConfig.Origins must contain at least one origin".to_string());
12026        }
12027        let origins = Origins {
12028            quantity: origin_entries.len() as i32,
12029            items: Some(OriginItems {
12030                origin: origin_entries,
12031            }),
12032        };
12033
12034        let dcb_value = cfg
12035            .get("DefaultCacheBehavior")
12036            .ok_or_else(|| "DistributionConfig.DefaultCacheBehavior is required".to_string())?;
12037        let default_cache_behavior: DefaultCacheBehavior =
12038            serde_json::from_value(dcb_value.clone())
12039                .map_err(|e| format!("Invalid DefaultCacheBehavior: {e}"))?;
12040
12041        let comment = cfg
12042            .get("Comment")
12043            .and_then(|v| v.as_str())
12044            .unwrap_or("")
12045            .to_string();
12046        let enabled = cfg.get("Enabled").and_then(|v| v.as_bool()).unwrap_or(true);
12047        let price_class = cfg
12048            .get("PriceClass")
12049            .and_then(|v| v.as_str())
12050            .map(|s| s.to_string());
12051        let http_version = cfg
12052            .get("HttpVersion")
12053            .and_then(|v| v.as_str())
12054            .map(|s| s.to_string());
12055        let is_ipv6_enabled = cfg.get("IPV6Enabled").and_then(|v| v.as_bool());
12056        let default_root_object = cfg
12057            .get("DefaultRootObject")
12058            .and_then(|v| v.as_str())
12059            .map(|s| s.to_string());
12060        let web_acl_id = cfg
12061            .get("WebACLId")
12062            .and_then(|v| v.as_str())
12063            .map(|s| s.to_string());
12064
12065        let viewer_certificate: Option<ViewerCertificate> = cfg
12066            .get("ViewerCertificate")
12067            .map(|v| serde_json::from_value(v.clone()))
12068            .transpose()
12069            .map_err(|e| format!("Invalid ViewerCertificate: {e}"))?;
12070
12071        let caller_reference = format!("cfn-{}-{}", resource.logical_id, Uuid::new_v4().simple());
12072
12073        let mut config = DistributionConfig {
12074            caller_reference,
12075            comment,
12076            enabled,
12077            origins,
12078            default_cache_behavior,
12079            ..Default::default()
12080        };
12081        config.price_class = price_class;
12082        config.http_version = http_version;
12083        config.is_ipv6_enabled = is_ipv6_enabled;
12084        config.default_root_object = default_root_object;
12085        config.web_acl_id = web_acl_id;
12086        config.viewer_certificate = viewer_certificate;
12087
12088        // Mint distribution id + ARN + domain in the same shape the
12089        // CloudFront service uses.
12090        let id_suffix: String = Uuid::new_v4()
12091            .simple()
12092            .to_string()
12093            .chars()
12094            .take(13)
12095            .collect::<String>()
12096            .to_uppercase();
12097        let id = format!("E{id_suffix}");
12098        let etag_suffix: String = Uuid::new_v4()
12099            .simple()
12100            .to_string()
12101            .chars()
12102            .take(7)
12103            .collect::<String>()
12104            .to_uppercase();
12105        let etag = format!("E{etag_suffix}");
12106        let domain_name = format!("{}.cloudfront.net", id.to_lowercase());
12107        let arn = format!(
12108            "arn:aws:cloudfront::{}:distribution/{}",
12109            self.account_id, id
12110        );
12111
12112        let stored = StoredDistribution {
12113            id: id.clone(),
12114            arn: arn.clone(),
12115            // CloudFront flips this to Deployed on the first GetDistribution
12116            // poll, matching the rest of the service.
12117            status: "InProgress".to_string(),
12118            last_modified_time: Utc::now(),
12119            domain_name: domain_name.clone(),
12120            in_progress_invalidation_batches: 0,
12121            etag,
12122            config,
12123        };
12124
12125        let mut accounts = self.cloudfront_state.write();
12126        let state = accounts.entry("000000000000");
12127        state.distributions.insert(id.clone(), stored);
12128        Ok(ProvisionResult::new(id.clone())
12129            .with("Id", id)
12130            .with("DomainName", domain_name)
12131            .with("Arn", arn))
12132    }
12133
12134    fn delete_cf_distribution(&self, physical_id: &str) -> Result<(), String> {
12135        let mut accounts = self.cloudfront_state.write();
12136        let state = accounts.entry("000000000000");
12137        state.distributions.remove(physical_id);
12138        Ok(())
12139    }
12140
12141    fn create_cf_origin_access_control(
12142        &self,
12143        resource: &ResourceDefinition,
12144    ) -> Result<ProvisionResult, String> {
12145        let props = &resource.properties;
12146        let cfg = props
12147            .get("OriginAccessControlConfig")
12148            .ok_or("OriginAccessControlConfig is required")?;
12149        let name = cfg
12150            .get("Name")
12151            .and_then(|v| v.as_str())
12152            .ok_or("OriginAccessControlConfig.Name is required")?
12153            .to_string();
12154        let signing_protocol = cfg
12155            .get("SigningProtocol")
12156            .and_then(|v| v.as_str())
12157            .unwrap_or("sigv4")
12158            .to_string();
12159        let signing_behavior = cfg
12160            .get("SigningBehavior")
12161            .and_then(|v| v.as_str())
12162            .unwrap_or("always")
12163            .to_string();
12164        let origin_type = cfg
12165            .get("OriginAccessControlOriginType")
12166            .and_then(|v| v.as_str())
12167            .ok_or("OriginAccessControlConfig.OriginAccessControlOriginType is required")?
12168            .to_string();
12169        let description = cfg
12170            .get("Description")
12171            .and_then(|v| v.as_str())
12172            .map(String::from);
12173
12174        let id = format!(
12175            "E{}",
12176            Uuid::new_v4().simple().to_string()[..13].to_uppercase()
12177        );
12178        let etag = format!(
12179            "E{}",
12180            Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12181        );
12182        let oac = StoredOriginAccessControl {
12183            id: id.clone(),
12184            etag,
12185            config: OriginAccessControlConfig {
12186                name,
12187                description,
12188                signing_protocol,
12189                signing_behavior,
12190                origin_access_control_origin_type: origin_type,
12191            },
12192        };
12193
12194        let mut accounts = self.cloudfront_state.write();
12195        let state = accounts.entry("000000000000");
12196        state.origin_access_controls.insert(id.clone(), oac);
12197
12198        Ok(ProvisionResult::new(id.clone()).with("Id", id))
12199    }
12200
12201    fn delete_cf_origin_access_control(&self, physical_id: &str) -> Result<(), String> {
12202        let mut accounts = self.cloudfront_state.write();
12203        let state = accounts.entry("000000000000");
12204        state.origin_access_controls.remove(physical_id);
12205        Ok(())
12206    }
12207
12208    fn create_cf_public_key(
12209        &self,
12210        resource: &ResourceDefinition,
12211    ) -> Result<ProvisionResult, String> {
12212        let props = &resource.properties;
12213        let cfg = props
12214            .get("PublicKeyConfig")
12215            .ok_or("PublicKeyConfig is required")?;
12216        let name = cfg
12217            .get("Name")
12218            .and_then(|v| v.as_str())
12219            .ok_or("PublicKeyConfig.Name is required")?
12220            .to_string();
12221        let encoded_key = cfg
12222            .get("EncodedKey")
12223            .and_then(|v| v.as_str())
12224            .ok_or("PublicKeyConfig.EncodedKey is required")?
12225            .to_string();
12226        let comment = cfg
12227            .get("Comment")
12228            .and_then(|v| v.as_str())
12229            .map(String::from);
12230        let caller_reference = cfg
12231            .get("CallerReference")
12232            .and_then(|v| v.as_str())
12233            .unwrap_or("")
12234            .to_string();
12235        let caller_reference = if caller_reference.is_empty() {
12236            format!("cfn-{}", resource.logical_id)
12237        } else {
12238            caller_reference
12239        };
12240
12241        let id = format!(
12242            "K{}",
12243            Uuid::new_v4().simple().to_string()[..13].to_uppercase()
12244        );
12245        let etag = format!(
12246            "E{}",
12247            Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12248        );
12249
12250        let pk = StoredPublicKey {
12251            id: id.clone(),
12252            etag,
12253            created_time: Utc::now(),
12254            config: PublicKeyConfig {
12255                caller_reference,
12256                name,
12257                encoded_key,
12258                comment,
12259            },
12260        };
12261
12262        let mut accounts = self.cloudfront_state.write();
12263        let state = accounts.entry("000000000000");
12264        state.public_keys.insert(id.clone(), pk);
12265
12266        Ok(ProvisionResult::new(id.clone()).with("Id", id))
12267    }
12268
12269    fn delete_cf_public_key(&self, physical_id: &str) -> Result<(), String> {
12270        let mut accounts = self.cloudfront_state.write();
12271        let state = accounts.entry("000000000000");
12272        state.public_keys.remove(physical_id);
12273        Ok(())
12274    }
12275
12276    fn create_cf_key_group(
12277        &self,
12278        resource: &ResourceDefinition,
12279    ) -> Result<ProvisionResult, String> {
12280        let props = &resource.properties;
12281        let cfg = props
12282            .get("KeyGroupConfig")
12283            .ok_or("KeyGroupConfig is required")?;
12284        let name = cfg
12285            .get("Name")
12286            .and_then(|v| v.as_str())
12287            .ok_or("KeyGroupConfig.Name is required")?
12288            .to_string();
12289        let items: Vec<String> = cfg
12290            .get("Items")
12291            .and_then(|v| v.as_array())
12292            .map(|arr| {
12293                arr.iter()
12294                    .filter_map(|v| v.as_str().map(String::from))
12295                    .collect()
12296            })
12297            .unwrap_or_default();
12298        let comment = cfg
12299            .get("Comment")
12300            .and_then(|v| v.as_str())
12301            .map(String::from);
12302
12303        let id = format!(
12304            "KG{}",
12305            Uuid::new_v4().simple().to_string()[..12].to_uppercase()
12306        );
12307        let etag = format!(
12308            "E{}",
12309            Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12310        );
12311
12312        let kg = StoredKeyGroup {
12313            id: id.clone(),
12314            etag,
12315            last_modified_time: Utc::now(),
12316            config: KeyGroupConfig {
12317                name,
12318                items: KeyGroupItems { public_key: items },
12319                comment,
12320            },
12321        };
12322
12323        let mut accounts = self.cloudfront_state.write();
12324        let state = accounts.entry("000000000000");
12325        state.key_groups.insert(id.clone(), kg);
12326
12327        Ok(ProvisionResult::new(id.clone()).with("Id", id))
12328    }
12329
12330    fn delete_cf_key_group(&self, physical_id: &str) -> Result<(), String> {
12331        let mut accounts = self.cloudfront_state.write();
12332        let state = accounts.entry("000000000000");
12333        state.key_groups.remove(physical_id);
12334        Ok(())
12335    }
12336
12337    fn create_cf_function(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
12338        let props = &resource.properties;
12339        let name = props
12340            .get("Name")
12341            .and_then(|v| v.as_str())
12342            .ok_or("Name is required")?
12343            .to_string();
12344        let function_code = props
12345            .get("FunctionCode")
12346            .and_then(|v| v.as_str())
12347            .ok_or("FunctionCode is required")?
12348            .to_string();
12349        let cfg = props
12350            .get("FunctionConfig")
12351            .ok_or("FunctionConfig is required")?;
12352        let runtime = cfg
12353            .get("Runtime")
12354            .and_then(|v| v.as_str())
12355            .unwrap_or("cloudfront-js-2.0")
12356            .to_string();
12357        let comment = cfg
12358            .get("Comment")
12359            .and_then(|v| v.as_str())
12360            .map(String::from);
12361
12362        let id = format!(
12363            "FN{}",
12364            Uuid::new_v4().simple().to_string()[..12].to_uppercase()
12365        );
12366        let etag = format!(
12367            "E{}",
12368            Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12369        );
12370        let function_arn = format!("arn:aws:cloudfront::{}:function/{}", self.account_id, name);
12371
12372        let now = Utc::now();
12373        let func = StoredFunction {
12374            name: name.clone(),
12375            etag,
12376            status: "UNPUBLISHED".to_string(),
12377            stage: "DEVELOPMENT".to_string(),
12378            function_arn: function_arn.clone(),
12379            created_time: now,
12380            last_modified_time: now,
12381            config: FunctionConfig {
12382                comment,
12383                runtime,
12384                key_value_store_associations: None,
12385            },
12386            function_code,
12387            live_function_code: None,
12388        };
12389
12390        let mut accounts = self.cloudfront_state.write();
12391        let state = accounts.entry("000000000000");
12392        // Use the function's ARN/name as the registry key so subsequent
12393        // operations (Get/Update/Delete) keyed by name resolve.
12394        state.functions.insert(name.clone(), func);
12395
12396        Ok(ProvisionResult::new(name.clone())
12397            .with("FunctionARN", function_arn)
12398            .with("FunctionMetadata.FunctionARN", id)
12399            .with("Stage", "DEVELOPMENT"))
12400    }
12401
12402    fn delete_cf_function(&self, physical_id: &str) -> Result<(), String> {
12403        let mut accounts = self.cloudfront_state.write();
12404        let state = accounts.entry("000000000000");
12405        state.functions.remove(physical_id);
12406        Ok(())
12407    }
12408
12409    fn create_cf_cache_policy(
12410        &self,
12411        resource: &ResourceDefinition,
12412    ) -> Result<ProvisionResult, String> {
12413        let props = &resource.properties;
12414        let cfg = props
12415            .get("CachePolicyConfig")
12416            .ok_or("CachePolicyConfig is required")?;
12417        let name = cfg
12418            .get("Name")
12419            .and_then(|v| v.as_str())
12420            .ok_or("CachePolicyConfig.Name is required")?
12421            .to_string();
12422        let min_ttl = cfg
12423            .get("MinTTL")
12424            .and_then(|v| {
12425                v.as_i64()
12426                    .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
12427            })
12428            .unwrap_or(0);
12429        let default_ttl = cfg.get("DefaultTTL").and_then(|v| {
12430            v.as_i64()
12431                .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
12432        });
12433        let max_ttl = cfg.get("MaxTTL").and_then(|v| {
12434            v.as_i64()
12435                .or_else(|| v.as_str().and_then(|s| s.parse::<i64>().ok()))
12436        });
12437        let comment = cfg
12438            .get("Comment")
12439            .and_then(|v| v.as_str())
12440            .map(String::from);
12441
12442        let id = format!(
12443            "CP{}",
12444            Uuid::new_v4().simple().to_string()[..12].to_uppercase()
12445        );
12446        let etag = format!(
12447            "E{}",
12448            Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12449        );
12450
12451        let cache_policy = StoredCachePolicy {
12452            id: id.clone(),
12453            etag,
12454            last_modified_time: Utc::now(),
12455            config: CachePolicyConfig {
12456                comment,
12457                name,
12458                default_ttl,
12459                max_ttl,
12460                min_ttl,
12461                parameters_in_cache_key_and_forwarded_to_origin: None,
12462            },
12463            policy_type: "custom".to_string(),
12464        };
12465
12466        let mut accounts = self.cloudfront_state.write();
12467        let state = accounts.entry("000000000000");
12468        state.cache_policies.insert(id.clone(), cache_policy);
12469
12470        Ok(ProvisionResult::new(id.clone()).with("Id", id))
12471    }
12472
12473    fn delete_cf_cache_policy(&self, physical_id: &str) -> Result<(), String> {
12474        let mut accounts = self.cloudfront_state.write();
12475        let state = accounts.entry("000000000000");
12476        state.cache_policies.remove(physical_id);
12477        Ok(())
12478    }
12479
12480    fn create_cf_origin_request_policy(
12481        &self,
12482        resource: &ResourceDefinition,
12483    ) -> Result<ProvisionResult, String> {
12484        let props = &resource.properties;
12485        let cfg = props
12486            .get("OriginRequestPolicyConfig")
12487            .ok_or("OriginRequestPolicyConfig is required")?;
12488        let name = cfg
12489            .get("Name")
12490            .and_then(|v| v.as_str())
12491            .ok_or("OriginRequestPolicyConfig.Name is required")?
12492            .to_string();
12493        let header_behavior = cfg
12494            .get("HeadersConfig")
12495            .and_then(|v| v.get("HeaderBehavior"))
12496            .and_then(|v| v.as_str())
12497            .unwrap_or("none")
12498            .to_string();
12499        let cookie_behavior = cfg
12500            .get("CookiesConfig")
12501            .and_then(|v| v.get("CookieBehavior"))
12502            .and_then(|v| v.as_str())
12503            .unwrap_or("none")
12504            .to_string();
12505        let query_string_behavior = cfg
12506            .get("QueryStringsConfig")
12507            .and_then(|v| v.get("QueryStringBehavior"))
12508            .and_then(|v| v.as_str())
12509            .unwrap_or("none")
12510            .to_string();
12511        let comment = cfg
12512            .get("Comment")
12513            .and_then(|v| v.as_str())
12514            .map(String::from);
12515
12516        let id = format!(
12517            "ORP{}",
12518            Uuid::new_v4().simple().to_string()[..11].to_uppercase()
12519        );
12520        let etag = format!(
12521            "E{}",
12522            Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12523        );
12524
12525        let policy = StoredOriginRequestPolicy {
12526            id: id.clone(),
12527            etag,
12528            last_modified_time: Utc::now(),
12529            config: OriginRequestPolicyConfig {
12530                comment,
12531                name,
12532                headers_config: OriginRequestPolicyHeadersConfig {
12533                    header_behavior,
12534                    headers: None,
12535                },
12536                cookies_config: OriginRequestPolicyCookiesConfig {
12537                    cookie_behavior,
12538                    cookies: None,
12539                },
12540                query_strings_config: OriginRequestPolicyQueryStringsConfig {
12541                    query_string_behavior,
12542                    query_strings: None,
12543                },
12544            },
12545            policy_type: "custom".to_string(),
12546        };
12547
12548        let mut accounts = self.cloudfront_state.write();
12549        let state = accounts.entry("000000000000");
12550        state.origin_request_policies.insert(id.clone(), policy);
12551
12552        Ok(ProvisionResult::new(id.clone()).with("Id", id))
12553    }
12554
12555    fn delete_cf_origin_request_policy(&self, physical_id: &str) -> Result<(), String> {
12556        let mut accounts = self.cloudfront_state.write();
12557        let state = accounts.entry("000000000000");
12558        state.origin_request_policies.remove(physical_id);
12559        Ok(())
12560    }
12561
12562    fn create_cf_response_headers_policy(
12563        &self,
12564        resource: &ResourceDefinition,
12565    ) -> Result<ProvisionResult, String> {
12566        let props = &resource.properties;
12567        let cfg = props
12568            .get("ResponseHeadersPolicyConfig")
12569            .ok_or("ResponseHeadersPolicyConfig is required")?;
12570        let name = cfg
12571            .get("Name")
12572            .and_then(|v| v.as_str())
12573            .ok_or("ResponseHeadersPolicyConfig.Name is required")?
12574            .to_string();
12575        let comment = cfg
12576            .get("Comment")
12577            .and_then(|v| v.as_str())
12578            .map(String::from);
12579
12580        let id = format!(
12581            "RHP{}",
12582            Uuid::new_v4().simple().to_string()[..11].to_uppercase()
12583        );
12584        let etag = format!(
12585            "E{}",
12586            Uuid::new_v4().simple().to_string()[..7].to_uppercase()
12587        );
12588
12589        let policy = StoredResponseHeadersPolicy {
12590            id: id.clone(),
12591            etag,
12592            last_modified_time: Utc::now(),
12593            config: ResponseHeadersPolicyConfig {
12594                comment,
12595                name,
12596                cors_config: None,
12597                security_headers_config: None,
12598                server_timing_headers_config: None,
12599                custom_headers_config: None,
12600                remove_headers_config: None,
12601            },
12602            policy_type: "custom".to_string(),
12603        };
12604
12605        let mut accounts = self.cloudfront_state.write();
12606        let state = accounts.entry("000000000000");
12607        state.response_headers_policies.insert(id.clone(), policy);
12608
12609        Ok(ProvisionResult::new(id.clone()).with("Id", id))
12610    }
12611
12612    fn delete_cf_response_headers_policy(&self, physical_id: &str) -> Result<(), String> {
12613        let mut accounts = self.cloudfront_state.write();
12614        let state = accounts.entry("000000000000");
12615        state.response_headers_policies.remove(physical_id);
12616        Ok(())
12617    }
12618
12619    // --- Step Functions ---
12620
12621    fn create_sfn_state_machine(
12622        &self,
12623        resource: &ResourceDefinition,
12624    ) -> Result<ProvisionResult, String> {
12625        let props = &resource.properties;
12626        let name = props
12627            .get("StateMachineName")
12628            .and_then(|v| v.as_str())
12629            .map(String::from)
12630            .unwrap_or_else(|| {
12631                let suffix = Uuid::new_v4().simple().to_string();
12632                format!("{}-{}", resource.logical_id, &suffix[..8])
12633            });
12634        let role_arn = props
12635            .get("RoleArn")
12636            .and_then(|v| v.as_str())
12637            .ok_or("RoleArn is required")?
12638            .to_string();
12639        let machine_type_str = props
12640            .get("StateMachineType")
12641            .and_then(|v| v.as_str())
12642            .unwrap_or("STANDARD");
12643        let machine_type = StateMachineType::parse(machine_type_str)
12644            .ok_or_else(|| format!("Invalid StateMachineType: {machine_type_str}"))?;
12645        let definition = props
12646            .get("DefinitionString")
12647            .and_then(|v| v.as_str())
12648            .map(String::from)
12649            .or_else(|| {
12650                props
12651                    .get("Definition")
12652                    .map(|v| serde_json::to_string(v).unwrap_or_default())
12653            })
12654            .ok_or("Definition or DefinitionString is required")?;
12655        let logging_configuration = props.get("LoggingConfiguration").cloned();
12656        let tracing_configuration = props.get("TracingConfiguration").cloned();
12657
12658        let arn = format!(
12659            "arn:aws:states:{}:{}:stateMachine:{}",
12660            self.region, self.account_id, name
12661        );
12662        let now = Utc::now();
12663        let revision_id = Uuid::new_v4().to_string();
12664
12665        let sm = StateMachine {
12666            name: name.clone(),
12667            arn: arn.clone(),
12668            definition,
12669            role_arn,
12670            machine_type,
12671            status: StateMachineStatus::Active,
12672            creation_date: now,
12673            update_date: now,
12674            tags: BTreeMap::new(),
12675            revision_id,
12676            logging_configuration,
12677            tracing_configuration,
12678            description: String::new(),
12679        };
12680
12681        let mut accounts = self.stepfunctions_state.write();
12682        let state = accounts.get_or_create(&self.account_id);
12683        state.state_machines.insert(arn.clone(), sm);
12684
12685        Ok(ProvisionResult::new(arn.clone())
12686            .with("Arn", arn.clone())
12687            .with("Name", name)
12688            .with("StateMachineRevisionId", "INITIAL"))
12689    }
12690
12691    fn delete_sfn_state_machine(&self, physical_id: &str) -> Result<(), String> {
12692        let mut accounts = self.stepfunctions_state.write();
12693        let state = accounts.get_or_create(&self.account_id);
12694        state.state_machines.remove(physical_id);
12695        Ok(())
12696    }
12697
12698    fn create_sfn_activity(
12699        &self,
12700        resource: &ResourceDefinition,
12701    ) -> Result<ProvisionResult, String> {
12702        let props = &resource.properties;
12703        let name = props
12704            .get("Name")
12705            .and_then(|v| v.as_str())
12706            .ok_or("Name is required")?
12707            .to_string();
12708        let arn = format!(
12709            "arn:aws:states:{}:{}:activity:{}",
12710            self.region, self.account_id, name
12711        );
12712        let activity = SfnActivity {
12713            name: name.clone(),
12714            arn: arn.clone(),
12715            creation_date: Utc::now(),
12716            tags: BTreeMap::new(),
12717        };
12718
12719        let mut accounts = self.stepfunctions_state.write();
12720        let state = accounts.get_or_create(&self.account_id);
12721        state.activities.insert(arn.clone(), activity);
12722
12723        Ok(ProvisionResult::new(arn.clone())
12724            .with("Arn", arn)
12725            .with("Name", name))
12726    }
12727
12728    fn delete_sfn_activity(&self, physical_id: &str) -> Result<(), String> {
12729        let mut accounts = self.stepfunctions_state.write();
12730        let state = accounts.get_or_create(&self.account_id);
12731        state.activities.remove(physical_id);
12732        Ok(())
12733    }
12734
12735    fn create_sfn_version(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
12736        let props = &resource.properties;
12737        let sm_arn = props
12738            .get("StateMachineArn")
12739            .and_then(|v| v.as_str())
12740            .ok_or("StateMachineArn is required")?
12741            .to_string();
12742        let description = props
12743            .get("Description")
12744            .and_then(|v| v.as_str())
12745            .unwrap_or("")
12746            .to_string();
12747        let revision_id = props
12748            .get("StateMachineRevisionId")
12749            .and_then(|v| v.as_str())
12750            .unwrap_or("INITIAL")
12751            .to_string();
12752
12753        let mut accounts = self.stepfunctions_state.write();
12754        let state = accounts.get_or_create(&self.account_id);
12755
12756        // Derive next version number for this state machine.
12757        let next_version = state
12758            .state_machine_versions
12759            .values()
12760            .filter(|v| v.state_machine_arn == sm_arn)
12761            .map(|v| v.version)
12762            .max()
12763            .unwrap_or(0)
12764            + 1;
12765        let version_arn = format!("{sm_arn}:{next_version}");
12766
12767        let version = StateMachineVersion {
12768            state_machine_arn: sm_arn,
12769            version: next_version,
12770            revision_id,
12771            description,
12772            creation_date: Utc::now(),
12773        };
12774        state
12775            .state_machine_versions
12776            .insert(version_arn.clone(), version);
12777
12778        Ok(ProvisionResult::new(version_arn.clone()).with("Arn", version_arn))
12779    }
12780
12781    fn delete_sfn_version(&self, physical_id: &str) -> Result<(), String> {
12782        let mut accounts = self.stepfunctions_state.write();
12783        let state = accounts.get_or_create(&self.account_id);
12784        state.state_machine_versions.remove(physical_id);
12785        Ok(())
12786    }
12787
12788    fn create_sfn_alias(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
12789        let props = &resource.properties;
12790        let name = props
12791            .get("Name")
12792            .and_then(|v| v.as_str())
12793            .ok_or("Name is required")?
12794            .to_string();
12795        let description = props
12796            .get("Description")
12797            .and_then(|v| v.as_str())
12798            .unwrap_or("")
12799            .to_string();
12800        let routes_value = props
12801            .get("RoutingConfiguration")
12802            .and_then(|v| v.as_array())
12803            .ok_or("RoutingConfiguration is required")?;
12804        let routing_configuration: Vec<AliasRoute> = routes_value
12805            .iter()
12806            .map(|r| AliasRoute {
12807                state_machine_version_arn: r
12808                    .get("StateMachineVersionArn")
12809                    .and_then(|x| x.as_str())
12810                    .unwrap_or("")
12811                    .to_string(),
12812                weight: r
12813                    .get("Weight")
12814                    .and_then(|x| {
12815                        x.as_i64()
12816                            .or_else(|| x.as_str().and_then(|s| s.parse::<i64>().ok()))
12817                    })
12818                    .map(|w| w as i32)
12819                    .unwrap_or(0),
12820            })
12821            .collect();
12822
12823        let first_version_arn = routing_configuration
12824            .first()
12825            .map(|r| r.state_machine_version_arn.clone())
12826            .unwrap_or_default();
12827        // Alias ARN derives from the parent state machine ARN (everything
12828        // before `:<version>`) + the alias name.
12829        let sm_arn_root = first_version_arn
12830            .rsplit_once(':')
12831            .map(|(root, _)| root.to_string())
12832            .unwrap_or_else(|| {
12833                format!(
12834                    "arn:aws:states:{}:{}:stateMachine:unknown",
12835                    self.region, self.account_id
12836                )
12837            });
12838        let arn = format!("{sm_arn_root}:{name}");
12839        let now = Utc::now();
12840        let alias = StateMachineAlias {
12841            name: name.clone(),
12842            arn: arn.clone(),
12843            description,
12844            routing_configuration,
12845            creation_date: now,
12846            update_date: now,
12847        };
12848
12849        let mut accounts = self.stepfunctions_state.write();
12850        let state = accounts.get_or_create(&self.account_id);
12851        state.state_machine_aliases.insert(arn.clone(), alias);
12852
12853        Ok(ProvisionResult::new(arn.clone())
12854            .with("Arn", arn)
12855            .with("Name", name))
12856    }
12857
12858    fn delete_sfn_alias(&self, physical_id: &str) -> Result<(), String> {
12859        let mut accounts = self.stepfunctions_state.write();
12860        let state = accounts.get_or_create(&self.account_id);
12861        state.state_machine_aliases.remove(physical_id);
12862        Ok(())
12863    }
12864
12865    // --- WAFv2 ---
12866    //
12867    // CFN exclusively writes WAFv2 resources at the global scope
12868    // (`CLOUDFRONT`) for global resources or `REGIONAL` for everything
12869    // else. We honor whatever the template specifies via the `Scope`
12870    // property and store under (scope, name).
12871
12872    fn create_wafv2_web_acl(
12873        &self,
12874        resource: &ResourceDefinition,
12875    ) -> Result<ProvisionResult, String> {
12876        let props = &resource.properties;
12877        let name = props
12878            .get("Name")
12879            .and_then(|v| v.as_str())
12880            .ok_or("Name is required")?
12881            .to_string();
12882        let scope = props
12883            .get("Scope")
12884            .and_then(|v| v.as_str())
12885            .ok_or("Scope is required")?
12886            .to_string();
12887        let default_action = props
12888            .get("DefaultAction")
12889            .cloned()
12890            .unwrap_or_else(|| serde_json::json!({"Allow": {}}));
12891        let description = props
12892            .get("Description")
12893            .and_then(|v| v.as_str())
12894            .map(String::from);
12895        let rules = props
12896            .get("Rules")
12897            .and_then(|v| v.as_array())
12898            .cloned()
12899            .unwrap_or_default();
12900        let visibility_config = props
12901            .get("VisibilityConfig")
12902            .cloned()
12903            .unwrap_or_else(|| serde_json::json!({}));
12904        let capacity = props.get("Capacity").and_then(|v| v.as_i64()).unwrap_or(0);
12905
12906        let id = Uuid::new_v4().to_string();
12907        let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
12908            ("us-east-1", "global".to_string())
12909        } else {
12910            (self.region.as_str(), self.region.clone())
12911        };
12912        let arn = format!(
12913            "arn:aws:wafv2:{}:{}:{}/webacl/{}/{}",
12914            region_in_arn, self.account_id, scope_seg, name, id
12915        );
12916        let acl = WebAcl {
12917            id: id.clone(),
12918            name: name.clone(),
12919            arn: arn.clone(),
12920            scope: scope.clone(),
12921            default_action,
12922            description,
12923            rules,
12924            visibility_config,
12925            capacity,
12926            lock_token: Uuid::new_v4().simple().to_string(),
12927            label_namespace: format!("awswaf:{}:webacl:{}:", self.account_id, name),
12928            custom_response_bodies: BTreeMap::new(),
12929            captcha_config: None,
12930            challenge_config: None,
12931            token_domains: Vec::new(),
12932            association_config: None,
12933            data_protection_config: None,
12934            on_source_d_do_s_protection_config: None,
12935            application_config: None,
12936            retrofitted_by_firewall_manager: false,
12937            pre_process_firewall_manager_rule_groups: Vec::new(),
12938            post_process_firewall_manager_rule_groups: Vec::new(),
12939            managed_by_firewall_manager: false,
12940            created_time: Utc::now(),
12941        };
12942
12943        let mut accounts = self.wafv2_state.write();
12944        let state = accounts
12945            .accounts
12946            .entry(self.account_id.clone())
12947            .or_default();
12948        state.web_acls.insert((scope.clone(), name.clone()), acl);
12949
12950        Ok(ProvisionResult::new(arn.clone())
12951            .with("Arn", arn)
12952            .with("Id", id)
12953            .with("Name", name)
12954            .with("Capacity", capacity.to_string()))
12955    }
12956
12957    fn delete_wafv2_web_acl(&self, physical_id: &str) -> Result<(), String> {
12958        let mut accounts = self.wafv2_state.write();
12959        let state = accounts
12960            .accounts
12961            .entry(self.account_id.clone())
12962            .or_default();
12963        state.web_acls.retain(|_, v| v.arn != physical_id);
12964        Ok(())
12965    }
12966
12967    fn create_wafv2_ip_set(
12968        &self,
12969        resource: &ResourceDefinition,
12970    ) -> Result<ProvisionResult, String> {
12971        let props = &resource.properties;
12972        let name = props
12973            .get("Name")
12974            .and_then(|v| v.as_str())
12975            .ok_or("Name is required")?
12976            .to_string();
12977        let scope = props
12978            .get("Scope")
12979            .and_then(|v| v.as_str())
12980            .ok_or("Scope is required")?
12981            .to_string();
12982        let ip_address_version = props
12983            .get("IPAddressVersion")
12984            .and_then(|v| v.as_str())
12985            .ok_or("IPAddressVersion is required")?
12986            .to_string();
12987        let addresses: Vec<String> = props
12988            .get("Addresses")
12989            .and_then(|v| v.as_array())
12990            .map(|arr| {
12991                arr.iter()
12992                    .filter_map(|v| v.as_str().map(String::from))
12993                    .collect()
12994            })
12995            .unwrap_or_default();
12996        let description = props
12997            .get("Description")
12998            .and_then(|v| v.as_str())
12999            .map(String::from);
13000
13001        let id = Uuid::new_v4().to_string();
13002        let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
13003            ("us-east-1", "global".to_string())
13004        } else {
13005            (self.region.as_str(), self.region.clone())
13006        };
13007        let arn = format!(
13008            "arn:aws:wafv2:{}:{}:{}/ipset/{}/{}",
13009            region_in_arn, self.account_id, scope_seg, name, id
13010        );
13011        let ip_set = IpSet {
13012            id: id.clone(),
13013            name: name.clone(),
13014            arn: arn.clone(),
13015            scope: scope.clone(),
13016            description,
13017            ip_address_version,
13018            addresses,
13019            lock_token: Uuid::new_v4().simple().to_string(),
13020            created_time: Utc::now(),
13021        };
13022
13023        let mut accounts = self.wafv2_state.write();
13024        let state = accounts
13025            .accounts
13026            .entry(self.account_id.clone())
13027            .or_default();
13028        state.ip_sets.insert((scope, name.clone()), ip_set);
13029
13030        Ok(ProvisionResult::new(arn.clone())
13031            .with("Arn", arn)
13032            .with("Id", id)
13033            .with("Name", name))
13034    }
13035
13036    fn delete_wafv2_ip_set(&self, physical_id: &str) -> Result<(), String> {
13037        let mut accounts = self.wafv2_state.write();
13038        let state = accounts
13039            .accounts
13040            .entry(self.account_id.clone())
13041            .or_default();
13042        state.ip_sets.retain(|_, v| v.arn != physical_id);
13043        Ok(())
13044    }
13045
13046    fn create_wafv2_regex_pattern_set(
13047        &self,
13048        resource: &ResourceDefinition,
13049    ) -> Result<ProvisionResult, String> {
13050        let props = &resource.properties;
13051        let name = props
13052            .get("Name")
13053            .and_then(|v| v.as_str())
13054            .ok_or("Name is required")?
13055            .to_string();
13056        let scope = props
13057            .get("Scope")
13058            .and_then(|v| v.as_str())
13059            .ok_or("Scope is required")?
13060            .to_string();
13061        let regular_expressions: Vec<serde_json::Value> = props
13062            .get("RegularExpressionList")
13063            .and_then(|v| v.as_array())
13064            .map(|arr| {
13065                arr.iter()
13066                    .map(|s| {
13067                        if let Some(s) = s.as_str() {
13068                            serde_json::json!({"RegexString": s})
13069                        } else {
13070                            s.clone()
13071                        }
13072                    })
13073                    .collect()
13074            })
13075            .unwrap_or_default();
13076        let description = props
13077            .get("Description")
13078            .and_then(|v| v.as_str())
13079            .map(String::from);
13080
13081        let id = Uuid::new_v4().to_string();
13082        let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
13083            ("us-east-1", "global".to_string())
13084        } else {
13085            (self.region.as_str(), self.region.clone())
13086        };
13087        let arn = format!(
13088            "arn:aws:wafv2:{}:{}:{}/regexpatternset/{}/{}",
13089            region_in_arn, self.account_id, scope_seg, name, id
13090        );
13091        let set = RegexPatternSet {
13092            id: id.clone(),
13093            name: name.clone(),
13094            arn: arn.clone(),
13095            scope: scope.clone(),
13096            description,
13097            regular_expressions,
13098            lock_token: Uuid::new_v4().simple().to_string(),
13099            created_time: Utc::now(),
13100        };
13101
13102        let mut accounts = self.wafv2_state.write();
13103        let state = accounts
13104            .accounts
13105            .entry(self.account_id.clone())
13106            .or_default();
13107        state.regex_pattern_sets.insert((scope, name.clone()), set);
13108
13109        Ok(ProvisionResult::new(arn.clone())
13110            .with("Arn", arn)
13111            .with("Id", id)
13112            .with("Name", name))
13113    }
13114
13115    fn delete_wafv2_regex_pattern_set(&self, physical_id: &str) -> Result<(), String> {
13116        let mut accounts = self.wafv2_state.write();
13117        let state = accounts
13118            .accounts
13119            .entry(self.account_id.clone())
13120            .or_default();
13121        state.regex_pattern_sets.retain(|_, v| v.arn != physical_id);
13122        Ok(())
13123    }
13124
13125    fn create_wafv2_rule_group(
13126        &self,
13127        resource: &ResourceDefinition,
13128    ) -> Result<ProvisionResult, String> {
13129        let props = &resource.properties;
13130        let name = props
13131            .get("Name")
13132            .and_then(|v| v.as_str())
13133            .ok_or("Name is required")?
13134            .to_string();
13135        let scope = props
13136            .get("Scope")
13137            .and_then(|v| v.as_str())
13138            .ok_or("Scope is required")?
13139            .to_string();
13140        let capacity = props
13141            .get("Capacity")
13142            .and_then(|v| v.as_i64())
13143            .ok_or("Capacity is required")?;
13144        let description = props
13145            .get("Description")
13146            .and_then(|v| v.as_str())
13147            .map(String::from);
13148        let rules = props
13149            .get("Rules")
13150            .and_then(|v| v.as_array())
13151            .cloned()
13152            .unwrap_or_default();
13153        let visibility_config = props
13154            .get("VisibilityConfig")
13155            .cloned()
13156            .unwrap_or_else(|| serde_json::json!({}));
13157
13158        let id = Uuid::new_v4().to_string();
13159        let (region_in_arn, scope_seg): (&str, String) = if scope == "CLOUDFRONT" {
13160            ("us-east-1", "global".to_string())
13161        } else {
13162            (self.region.as_str(), self.region.clone())
13163        };
13164        let arn = format!(
13165            "arn:aws:wafv2:{}:{}:{}/rulegroup/{}/{}",
13166            region_in_arn, self.account_id, scope_seg, name, id
13167        );
13168        let rg = RuleGroup {
13169            id: id.clone(),
13170            name: name.clone(),
13171            arn: arn.clone(),
13172            scope: scope.clone(),
13173            capacity,
13174            description,
13175            rules,
13176            visibility_config,
13177            lock_token: Uuid::new_v4().simple().to_string(),
13178            label_namespace: format!("awswaf:{}:rulegroup:{}:", self.account_id, name),
13179            custom_response_bodies: BTreeMap::new(),
13180            available_labels: Vec::new(),
13181            consumed_labels: Vec::new(),
13182            created_time: Utc::now(),
13183        };
13184
13185        let mut accounts = self.wafv2_state.write();
13186        let state = accounts
13187            .accounts
13188            .entry(self.account_id.clone())
13189            .or_default();
13190        state.rule_groups.insert((scope, name.clone()), rg);
13191
13192        Ok(ProvisionResult::new(arn.clone())
13193            .with("Arn", arn)
13194            .with("Id", id)
13195            .with("Name", name)
13196            .with("Capacity", capacity.to_string()))
13197    }
13198
13199    fn delete_wafv2_rule_group(&self, physical_id: &str) -> Result<(), String> {
13200        let mut accounts = self.wafv2_state.write();
13201        let state = accounts
13202            .accounts
13203            .entry(self.account_id.clone())
13204            .or_default();
13205        state.rule_groups.retain(|_, v| v.arn != physical_id);
13206        Ok(())
13207    }
13208
13209    fn create_wafv2_logging_configuration(
13210        &self,
13211        resource: &ResourceDefinition,
13212    ) -> Result<ProvisionResult, String> {
13213        let props = &resource.properties;
13214        let resource_arn = props
13215            .get("ResourceArn")
13216            .and_then(|v| v.as_str())
13217            .ok_or("ResourceArn is required")?
13218            .to_string();
13219        let cfg = serde_json::json!({
13220            "ResourceArn": resource_arn,
13221            "LogDestinationConfigs": props.get("LogDestinationConfigs").cloned().unwrap_or_else(|| serde_json::json!([])),
13222            "RedactedFields": props.get("RedactedFields").cloned().unwrap_or_else(|| serde_json::json!([])),
13223            "LoggingFilter": props.get("LoggingFilter").cloned(),
13224        });
13225
13226        let mut accounts = self.wafv2_state.write();
13227        let state = accounts
13228            .accounts
13229            .entry(self.account_id.clone())
13230            .or_default();
13231        state.logging_configs.insert(resource_arn.clone(), cfg);
13232
13233        Ok(ProvisionResult::new(resource_arn))
13234    }
13235
13236    fn delete_wafv2_logging_configuration(&self, physical_id: &str) -> Result<(), String> {
13237        let mut accounts = self.wafv2_state.write();
13238        let state = accounts
13239            .accounts
13240            .entry(self.account_id.clone())
13241            .or_default();
13242        state.logging_configs.remove(physical_id);
13243        Ok(())
13244    }
13245
13246    fn create_wafv2_web_acl_association(
13247        &self,
13248        resource: &ResourceDefinition,
13249    ) -> Result<ProvisionResult, String> {
13250        let props = &resource.properties;
13251        let resource_arn = props
13252            .get("ResourceArn")
13253            .and_then(|v| v.as_str())
13254            .ok_or("ResourceArn is required")?
13255            .to_string();
13256        let web_acl_arn = props
13257            .get("WebACLArn")
13258            .and_then(|v| v.as_str())
13259            .ok_or("WebACLArn is required")?
13260            .to_string();
13261
13262        let mut accounts = self.wafv2_state.write();
13263        let state = accounts
13264            .accounts
13265            .entry(self.account_id.clone())
13266            .or_default();
13267        state.associations.insert(resource_arn.clone(), web_acl_arn);
13268
13269        // Physical id encodes the resource arn so delete can find it.
13270        Ok(ProvisionResult::new(resource_arn))
13271    }
13272
13273    fn delete_wafv2_web_acl_association(&self, physical_id: &str) -> Result<(), String> {
13274        let mut accounts = self.wafv2_state.write();
13275        let state = accounts
13276            .accounts
13277            .entry(self.account_id.clone())
13278            .or_default();
13279        state.associations.remove(physical_id);
13280        Ok(())
13281    }
13282
13283    // --- API Gateway v1 ---
13284
13285    fn create_apigw_rest_api(
13286        &self,
13287        resource: &ResourceDefinition,
13288    ) -> Result<ProvisionResult, String> {
13289        let props = &resource.properties;
13290        let name = props
13291            .get("Name")
13292            .and_then(|v| v.as_str())
13293            .ok_or("Name is required")?
13294            .to_string();
13295        let description = props
13296            .get("Description")
13297            .and_then(|v| v.as_str())
13298            .map(String::from);
13299        let api_key_source = props
13300            .get("ApiKeySourceType")
13301            .and_then(|v| v.as_str())
13302            .unwrap_or("HEADER")
13303            .to_string();
13304        let endpoint_configuration = props
13305            .get("EndpointConfiguration")
13306            .cloned()
13307            .unwrap_or_else(|| serde_json::json!({"types": ["EDGE"]}));
13308        let policy = props
13309            .get("Policy")
13310            .map(|v| v.to_string().trim_matches('"').to_string());
13311        let binary_media_types: Vec<String> = props
13312            .get("BinaryMediaTypes")
13313            .and_then(|v| v.as_array())
13314            .map(|arr| {
13315                arr.iter()
13316                    .filter_map(|v| v.as_str().map(String::from))
13317                    .collect()
13318            })
13319            .unwrap_or_default();
13320        let minimum_compression_size = props.get("MinimumCompressionSize").and_then(|v| v.as_i64());
13321        let disable_execute_api_endpoint = props
13322            .get("DisableExecuteApiEndpoint")
13323            .and_then(|v| v.as_bool())
13324            .unwrap_or(false);
13325        // CFN exposes optional `Body`/`BodyS3Location`/`CloneFrom` for OpenAPI
13326        // import. We don't run a full Swagger import — we record the source
13327        // in the api's `import_source` field so callers can reason about it.
13328        let import_source = if props.get("Body").is_some() {
13329            Some("Body".to_string())
13330        } else if props.get("BodyS3Location").is_some() {
13331            Some("BodyS3Location".to_string())
13332        } else if props.get("CloneFrom").is_some() {
13333            Some("CloneFrom".to_string())
13334        } else {
13335            None
13336        };
13337        let tags = parse_acm_tags(props.get("Tags"));
13338
13339        let id = apigw_make_id();
13340        let root_resource_id = apigw_make_id();
13341        let now = Utc::now();
13342
13343        let api = ApiGwRestApi {
13344            id: id.clone(),
13345            name,
13346            description,
13347            version: props
13348                .get("Version")
13349                .and_then(|v| v.as_str())
13350                .map(String::from),
13351            created_date: now,
13352            api_key_source,
13353            endpoint_configuration,
13354            policy,
13355            binary_media_types,
13356            minimum_compression_size,
13357            disable_execute_api_endpoint,
13358            root_resource_id: root_resource_id.clone(),
13359            tags,
13360            import_source,
13361        };
13362
13363        let mut accounts = self.apigateway_state.write();
13364        let state = accounts.get_or_create(&self.account_id);
13365        state.apis.insert(id.clone(), api);
13366        let mut resources = BTreeMap::new();
13367        resources.insert(
13368            root_resource_id.clone(),
13369            ApiGwResource {
13370                id: root_resource_id.clone(),
13371                parent_id: None,
13372                path_part: None,
13373                path: "/".to_string(),
13374            },
13375        );
13376        state.resources.insert(id.clone(), resources);
13377
13378        Ok(ProvisionResult::new(id.clone())
13379            .with("RestApiId", id.clone())
13380            .with("RootResourceId", root_resource_id))
13381    }
13382
13383    fn update_apigw_rest_api(
13384        &self,
13385        existing: &StackResource,
13386        resource: &ResourceDefinition,
13387    ) -> Result<ProvisionResult, String> {
13388        let props = &resource.properties;
13389        let id = existing.physical_id.clone();
13390        let mut accounts = self.apigateway_state.write();
13391        let state = accounts.get_or_create(&self.account_id);
13392        let api = state
13393            .apis
13394            .get_mut(&id)
13395            .ok_or_else(|| format!("RestApi {id} not found for update"))?;
13396        if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
13397            api.name = name.to_string();
13398        }
13399        if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
13400            api.description = Some(desc.to_string());
13401        }
13402        if let Some(source) = props.get("ApiKeySourceType").and_then(|v| v.as_str()) {
13403            api.api_key_source = source.to_string();
13404        }
13405        if let Some(ep) = props.get("EndpointConfiguration").cloned() {
13406            api.endpoint_configuration = ep;
13407        }
13408        if let Some(arr) = props.get("BinaryMediaTypes").and_then(|v| v.as_array()) {
13409            api.binary_media_types = arr
13410                .iter()
13411                .filter_map(|v| v.as_str().map(String::from))
13412                .collect();
13413        }
13414        if let Some(size) = props.get("MinimumCompressionSize").and_then(|v| v.as_i64()) {
13415            api.minimum_compression_size = Some(size);
13416        }
13417        if let Some(b) = props
13418            .get("DisableExecuteApiEndpoint")
13419            .and_then(|v| v.as_bool())
13420        {
13421            api.disable_execute_api_endpoint = b;
13422        }
13423        if props.get("Tags").is_some() {
13424            api.tags = parse_acm_tags(props.get("Tags"));
13425        }
13426        let root = api.root_resource_id.clone();
13427        Ok(ProvisionResult::new(id.clone())
13428            .with("RestApiId", id)
13429            .with("RootResourceId", root))
13430    }
13431
13432    fn delete_apigw_rest_api(&self, physical_id: &str) -> Result<(), String> {
13433        let mut accounts = self.apigateway_state.write();
13434        let state = accounts.get_or_create(&self.account_id);
13435        state.apis.remove(physical_id);
13436        state.resources.remove(physical_id);
13437        let prefix = format!("{physical_id}/");
13438        state.methods.retain(|k, _| !k.starts_with(&prefix));
13439        state.integrations.retain(|k, _| !k.starts_with(&prefix));
13440        state
13441            .integration_responses
13442            .retain(|k, _| !k.starts_with(&prefix));
13443        state
13444            .method_responses
13445            .retain(|k, _| !k.starts_with(&prefix));
13446        state.deployments.remove(physical_id);
13447        state.stages.remove(physical_id);
13448        state.models.remove(physical_id);
13449        state.request_validators.remove(physical_id);
13450        state.authorizers.remove(physical_id);
13451        state.gateway_responses.remove(physical_id);
13452        Ok(())
13453    }
13454
13455    fn create_apigw_resource(
13456        &self,
13457        resource: &ResourceDefinition,
13458    ) -> Result<ProvisionResult, String> {
13459        let props = &resource.properties;
13460        let rest_api_id = props
13461            .get("RestApiId")
13462            .and_then(|v| v.as_str())
13463            .ok_or("RestApiId is required")?
13464            .to_string();
13465        let parent_id = props
13466            .get("ParentId")
13467            .and_then(|v| v.as_str())
13468            .ok_or("ParentId is required")?
13469            .to_string();
13470        let path_part = props
13471            .get("PathPart")
13472            .and_then(|v| v.as_str())
13473            .ok_or("PathPart is required")?
13474            .to_string();
13475
13476        let mut accounts = self.apigateway_state.write();
13477        let state = accounts.get_or_create(&self.account_id);
13478        let api_resources = state
13479            .resources
13480            .get(&rest_api_id)
13481            .ok_or_else(|| format!("RestApi {rest_api_id} not found"))?;
13482        let parent = api_resources
13483            .get(&parent_id)
13484            .ok_or_else(|| format!("Parent resource {parent_id} not found"))?;
13485        let parent_path = parent.path.clone();
13486        let path = if parent_path == "/" {
13487            format!("/{path_part}")
13488        } else {
13489            format!("{parent_path}/{path_part}")
13490        };
13491
13492        let id = apigw_make_id();
13493        let new_resource = ApiGwResource {
13494            id: id.clone(),
13495            parent_id: Some(parent_id),
13496            path_part: Some(path_part),
13497            path,
13498        };
13499        state
13500            .resources
13501            .entry(rest_api_id.clone())
13502            .or_default()
13503            .insert(id.clone(), new_resource);
13504
13505        Ok(ProvisionResult::new(id.clone())
13506            .with("ResourceId", id)
13507            .with("RestApiId", rest_api_id))
13508    }
13509
13510    fn delete_apigw_resource(
13511        &self,
13512        physical_id: &str,
13513        attributes: &BTreeMap<String, String>,
13514    ) -> Result<(), String> {
13515        let Some(rest_api_id) = attributes.get("RestApiId") else {
13516            return Ok(());
13517        };
13518        let mut accounts = self.apigateway_state.write();
13519        let state = accounts.get_or_create(&self.account_id);
13520        if let Some(map) = state.resources.get_mut(rest_api_id) {
13521            map.remove(physical_id);
13522        }
13523        let prefix = format!("{rest_api_id}/{physical_id}/");
13524        state.methods.retain(|k, _| !k.starts_with(&prefix));
13525        state.integrations.retain(|k, _| !k.starts_with(&prefix));
13526        Ok(())
13527    }
13528
13529    fn create_apigw_method(
13530        &self,
13531        resource: &ResourceDefinition,
13532    ) -> Result<ProvisionResult, String> {
13533        let props = &resource.properties;
13534        let rest_api_id = props
13535            .get("RestApiId")
13536            .and_then(|v| v.as_str())
13537            .ok_or("RestApiId is required")?
13538            .to_string();
13539        let resource_id = props
13540            .get("ResourceId")
13541            .and_then(|v| v.as_str())
13542            .ok_or("ResourceId is required")?
13543            .to_string();
13544        let http_method = props
13545            .get("HttpMethod")
13546            .and_then(|v| v.as_str())
13547            .ok_or("HttpMethod is required")?
13548            .to_uppercase();
13549        let authorization_type = props
13550            .get("AuthorizationType")
13551            .and_then(|v| v.as_str())
13552            .unwrap_or("NONE")
13553            .to_string();
13554        let authorizer_id = props
13555            .get("AuthorizerId")
13556            .and_then(|v| v.as_str())
13557            .map(String::from);
13558        let api_key_required = props
13559            .get("ApiKeyRequired")
13560            .and_then(|v| v.as_bool())
13561            .unwrap_or(false);
13562        let operation_name = props
13563            .get("OperationName")
13564            .and_then(|v| v.as_str())
13565            .map(String::from);
13566        let request_validator_id = props
13567            .get("RequestValidatorId")
13568            .and_then(|v| v.as_str())
13569            .map(String::from);
13570        let request_parameters: BTreeMap<String, bool> = props
13571            .get("RequestParameters")
13572            .and_then(|v| v.as_object())
13573            .map(|obj| {
13574                obj.iter()
13575                    .map(|(k, v)| (k.clone(), v.as_bool().unwrap_or(false)))
13576                    .collect()
13577            })
13578            .unwrap_or_default();
13579        let request_models: BTreeMap<String, String> = props
13580            .get("RequestModels")
13581            .and_then(|v| v.as_object())
13582            .map(|obj| {
13583                obj.iter()
13584                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13585                    .collect()
13586            })
13587            .unwrap_or_default();
13588        let authorization_scopes: Vec<String> = props
13589            .get("AuthorizationScopes")
13590            .and_then(|v| v.as_array())
13591            .map(|arr| {
13592                arr.iter()
13593                    .filter_map(|v| v.as_str().map(String::from))
13594                    .collect()
13595            })
13596            .unwrap_or_default();
13597
13598        let composite_key = format!("{rest_api_id}/{resource_id}/{http_method}");
13599        let method = ApiGwMethod {
13600            rest_api_id: rest_api_id.clone(),
13601            resource_id: resource_id.clone(),
13602            http_method: http_method.clone(),
13603            authorization_type,
13604            authorizer_id,
13605            api_key_required,
13606            operation_name,
13607            request_parameters,
13608            request_models,
13609            request_validator_id,
13610            authorization_scopes,
13611        };
13612
13613        let mut accounts = self.apigateway_state.write();
13614        let state = accounts.get_or_create(&self.account_id);
13615        if !state.apis.contains_key(&rest_api_id) {
13616            return Err(format!("RestApi {rest_api_id} not found"));
13617        }
13618        // Multi-pass provisioning: if `Ref: SomeResource` resolved to the
13619        // logical id (because the referenced resource hasn't been
13620        // provisioned yet on this pass), bail so CFN retries us next pass.
13621        let resource_known = state
13622            .resources
13623            .get(&rest_api_id)
13624            .map(|m| m.contains_key(&resource_id))
13625            .unwrap_or(false);
13626        if !resource_known {
13627            return Err(format!(
13628                "Resource {resource_id} not yet provisioned for api {rest_api_id}"
13629            ));
13630        }
13631        state.methods.insert(composite_key.clone(), method);
13632
13633        if let Some(integ_props) = props.get("Integration").and_then(|v| v.as_object()) {
13634            let integration = ApiGwIntegration {
13635                rest_api_id: rest_api_id.clone(),
13636                resource_id: resource_id.clone(),
13637                http_method: http_method.clone(),
13638                integration_type: integ_props
13639                    .get("Type")
13640                    .and_then(|v| v.as_str())
13641                    .unwrap_or("MOCK")
13642                    .to_string(),
13643                integration_http_method: integ_props
13644                    .get("IntegrationHttpMethod")
13645                    .and_then(|v| v.as_str())
13646                    .map(String::from),
13647                uri: integ_props
13648                    .get("Uri")
13649                    .and_then(|v| v.as_str())
13650                    .map(String::from),
13651                credentials: integ_props
13652                    .get("Credentials")
13653                    .and_then(|v| v.as_str())
13654                    .map(String::from),
13655                request_parameters: integ_props
13656                    .get("RequestParameters")
13657                    .and_then(|v| v.as_object())
13658                    .map(|obj| {
13659                        obj.iter()
13660                            .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13661                            .collect()
13662                    })
13663                    .unwrap_or_default(),
13664                request_templates: integ_props
13665                    .get("RequestTemplates")
13666                    .and_then(|v| v.as_object())
13667                    .map(|obj| {
13668                        obj.iter()
13669                            .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13670                            .collect()
13671                    })
13672                    .unwrap_or_default(),
13673                passthrough_behavior: integ_props
13674                    .get("PassthroughBehavior")
13675                    .and_then(|v| v.as_str())
13676                    .unwrap_or("WHEN_NO_MATCH")
13677                    .to_string(),
13678                timeout_in_millis: integ_props
13679                    .get("TimeoutInMillis")
13680                    .and_then(|v| v.as_i64())
13681                    .map(|n| n as i32),
13682                cache_namespace: integ_props
13683                    .get("CacheNamespace")
13684                    .and_then(|v| v.as_str())
13685                    .map(String::from),
13686                cache_key_parameters: integ_props
13687                    .get("CacheKeyParameters")
13688                    .and_then(|v| v.as_array())
13689                    .map(|arr| {
13690                        arr.iter()
13691                            .filter_map(|v| v.as_str().map(String::from))
13692                            .collect()
13693                    })
13694                    .unwrap_or_default(),
13695                content_handling: integ_props
13696                    .get("ContentHandling")
13697                    .and_then(|v| v.as_str())
13698                    .map(String::from),
13699                connection_type: integ_props
13700                    .get("ConnectionType")
13701                    .and_then(|v| v.as_str())
13702                    .map(String::from),
13703                connection_id: integ_props
13704                    .get("ConnectionId")
13705                    .and_then(|v| v.as_str())
13706                    .map(String::from),
13707                tls_config: integ_props.get("TlsConfig").cloned(),
13708            };
13709            state
13710                .integrations
13711                .insert(composite_key.clone(), integration);
13712        }
13713
13714        Ok(ProvisionResult::new(composite_key.clone())
13715            .with("MethodKey", composite_key)
13716            .with("RestApiId", rest_api_id)
13717            .with("ResourceId", resource_id)
13718            .with("HttpMethod", http_method))
13719    }
13720
13721    fn delete_apigw_method(&self, physical_id: &str) -> Result<(), String> {
13722        let mut accounts = self.apigateway_state.write();
13723        let state = accounts.get_or_create(&self.account_id);
13724        state.methods.remove(physical_id);
13725        state.integrations.remove(physical_id);
13726        let prefix = format!("{physical_id}/");
13727        state
13728            .integration_responses
13729            .retain(|k, _| !k.starts_with(&prefix));
13730        state
13731            .method_responses
13732            .retain(|k, _| !k.starts_with(&prefix));
13733        Ok(())
13734    }
13735
13736    fn create_apigw_deployment(
13737        &self,
13738        resource: &ResourceDefinition,
13739    ) -> Result<ProvisionResult, String> {
13740        let props = &resource.properties;
13741        let rest_api_id = props
13742            .get("RestApiId")
13743            .and_then(|v| v.as_str())
13744            .ok_or("RestApiId is required")?
13745            .to_string();
13746        let description = props
13747            .get("Description")
13748            .and_then(|v| v.as_str())
13749            .map(String::from);
13750
13751        let id = apigw_make_id();
13752        let mut accounts = self.apigateway_state.write();
13753        let state = accounts.get_or_create(&self.account_id);
13754        if !state.apis.contains_key(&rest_api_id) {
13755            return Err(format!("RestApi {rest_api_id} not found"));
13756        }
13757        let api_summary = serde_json::to_value(
13758            state
13759                .resources
13760                .get(&rest_api_id)
13761                .cloned()
13762                .unwrap_or_default(),
13763        )
13764        .unwrap_or(serde_json::Value::Null);
13765        let deployment = ApiGwDeployment {
13766            id: id.clone(),
13767            description,
13768            created_date: Utc::now(),
13769            api_summary,
13770        };
13771        state
13772            .deployments
13773            .entry(rest_api_id.clone())
13774            .or_default()
13775            .insert(id.clone(), deployment);
13776
13777        // CFN inline `StageName` creates a Stage referencing this deployment.
13778        if let Some(stage_name) = props
13779            .get("StageName")
13780            .and_then(|v| v.as_str())
13781            .map(String::from)
13782        {
13783            let stage = ApiGwStage {
13784                stage_name: stage_name.clone(),
13785                deployment_id: id.clone(),
13786                description: props
13787                    .get("StageDescription")
13788                    .and_then(|v| v.get("Description"))
13789                    .and_then(|v| v.as_str())
13790                    .map(String::from),
13791                cache_cluster_enabled: false,
13792                cache_cluster_size: None,
13793                variables: BTreeMap::new(),
13794                method_settings: BTreeMap::new(),
13795                created_date: Utc::now(),
13796                last_updated_date: Utc::now(),
13797                tracing_enabled: false,
13798                web_acl_arn: None,
13799                canary_settings: None,
13800                access_log_settings: None,
13801                tags: BTreeMap::new(),
13802            };
13803            state
13804                .stages
13805                .entry(rest_api_id.clone())
13806                .or_default()
13807                .insert(stage_name, stage);
13808        }
13809
13810        Ok(ProvisionResult::new(id.clone())
13811            .with("DeploymentId", id)
13812            .with("RestApiId", rest_api_id))
13813    }
13814
13815    fn delete_apigw_deployment(
13816        &self,
13817        physical_id: &str,
13818        attributes: &BTreeMap<String, String>,
13819    ) -> Result<(), String> {
13820        let Some(rest_api_id) = attributes.get("RestApiId") else {
13821            return Ok(());
13822        };
13823        let mut accounts = self.apigateway_state.write();
13824        let state = accounts.get_or_create(&self.account_id);
13825        if let Some(map) = state.deployments.get_mut(rest_api_id) {
13826            map.remove(physical_id);
13827        }
13828        Ok(())
13829    }
13830
13831    fn create_apigw_stage(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
13832        let props = &resource.properties;
13833        let rest_api_id = props
13834            .get("RestApiId")
13835            .and_then(|v| v.as_str())
13836            .ok_or("RestApiId is required")?
13837            .to_string();
13838        let stage_name = props
13839            .get("StageName")
13840            .and_then(|v| v.as_str())
13841            .ok_or("StageName is required")?
13842            .to_string();
13843        let deployment_id = props
13844            .get("DeploymentId")
13845            .and_then(|v| v.as_str())
13846            .ok_or("DeploymentId is required")?
13847            .to_string();
13848
13849        let variables: BTreeMap<String, String> = props
13850            .get("Variables")
13851            .and_then(|v| v.as_object())
13852            .map(|obj| {
13853                obj.iter()
13854                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
13855                    .collect()
13856            })
13857            .unwrap_or_default();
13858        let tracing_enabled = props
13859            .get("TracingEnabled")
13860            .and_then(|v| v.as_bool())
13861            .unwrap_or(false);
13862        let cache_cluster_enabled = props
13863            .get("CacheClusterEnabled")
13864            .and_then(|v| v.as_bool())
13865            .unwrap_or(false);
13866        let cache_cluster_size = props
13867            .get("CacheClusterSize")
13868            .and_then(|v| v.as_str())
13869            .map(String::from);
13870        // CFN models MethodSettings as a list of `{ResourcePath,HttpMethod,...}`
13871        // entries; the live API stores them as a `path/method -> settings`
13872        // map. Translate by joining ResourcePath + HttpMethod into the key.
13873        let method_settings: BTreeMap<String, serde_json::Value> = props
13874            .get("MethodSettings")
13875            .and_then(|v| v.as_array())
13876            .map(|arr| {
13877                arr.iter()
13878                    .filter_map(|s| {
13879                        let path = s.get("ResourcePath").and_then(|v| v.as_str())?;
13880                        let http = s.get("HttpMethod").and_then(|v| v.as_str())?;
13881                        let key = format!("{}/{http}", path.strip_prefix('/').unwrap_or(path));
13882                        Some((key, s.clone()))
13883                    })
13884                    .collect()
13885            })
13886            .unwrap_or_default();
13887        let tags = parse_acm_tags(props.get("Tags"));
13888
13889        let stage = ApiGwStage {
13890            stage_name: stage_name.clone(),
13891            deployment_id,
13892            description: props
13893                .get("Description")
13894                .and_then(|v| v.as_str())
13895                .map(String::from),
13896            cache_cluster_enabled,
13897            cache_cluster_size,
13898            variables,
13899            method_settings,
13900            created_date: Utc::now(),
13901            last_updated_date: Utc::now(),
13902            tracing_enabled,
13903            web_acl_arn: None,
13904            canary_settings: props.get("CanarySetting").cloned(),
13905            access_log_settings: props.get("AccessLogSetting").cloned(),
13906            tags,
13907        };
13908
13909        let mut accounts = self.apigateway_state.write();
13910        let state = accounts.get_or_create(&self.account_id);
13911        if !state.apis.contains_key(&rest_api_id) {
13912            return Err(format!("RestApi {rest_api_id} not found"));
13913        }
13914        let dep_known = state
13915            .deployments
13916            .get(&rest_api_id)
13917            .map(|m| m.contains_key(&stage.deployment_id))
13918            .unwrap_or(false);
13919        if !dep_known {
13920            return Err(format!(
13921                "Deployment {} not yet provisioned for api {rest_api_id}",
13922                stage.deployment_id
13923            ));
13924        }
13925        state
13926            .stages
13927            .entry(rest_api_id.clone())
13928            .or_default()
13929            .insert(stage_name.clone(), stage);
13930
13931        Ok(ProvisionResult::new(stage_name.clone())
13932            .with("StageName", stage_name)
13933            .with("RestApiId", rest_api_id))
13934    }
13935
13936    fn delete_apigw_stage(
13937        &self,
13938        physical_id: &str,
13939        attributes: &BTreeMap<String, String>,
13940    ) -> Result<(), String> {
13941        let Some(rest_api_id) = attributes.get("RestApiId") else {
13942            return Ok(());
13943        };
13944        let mut accounts = self.apigateway_state.write();
13945        let state = accounts.get_or_create(&self.account_id);
13946        if let Some(map) = state.stages.get_mut(rest_api_id) {
13947            map.remove(physical_id);
13948        }
13949        Ok(())
13950    }
13951
13952    fn create_apigw_authorizer(
13953        &self,
13954        resource: &ResourceDefinition,
13955    ) -> Result<ProvisionResult, String> {
13956        let props = &resource.properties;
13957        let rest_api_id = props
13958            .get("RestApiId")
13959            .and_then(|v| v.as_str())
13960            .ok_or("RestApiId is required")?
13961            .to_string();
13962        let name = props
13963            .get("Name")
13964            .and_then(|v| v.as_str())
13965            .ok_or("Name is required")?
13966            .to_string();
13967        let authorizer_type = props
13968            .get("Type")
13969            .and_then(|v| v.as_str())
13970            .unwrap_or("TOKEN")
13971            .to_string();
13972        let provider_arns: Vec<String> = props
13973            .get("ProviderARNs")
13974            .and_then(|v| v.as_array())
13975            .map(|arr| {
13976                arr.iter()
13977                    .filter_map(|v| v.as_str().map(String::from))
13978                    .collect()
13979            })
13980            .unwrap_or_default();
13981
13982        let id = apigw_make_id();
13983        let auth = ApiGwAuthorizer {
13984            id: id.clone(),
13985            name,
13986            authorizer_type,
13987            provider_arns,
13988            auth_type: props
13989                .get("AuthType")
13990                .and_then(|v| v.as_str())
13991                .map(String::from),
13992            authorizer_uri: props
13993                .get("AuthorizerUri")
13994                .and_then(|v| v.as_str())
13995                .map(String::from),
13996            authorizer_credentials: props
13997                .get("AuthorizerCredentials")
13998                .and_then(|v| v.as_str())
13999                .map(String::from),
14000            identity_source: props
14001                .get("IdentitySource")
14002                .and_then(|v| v.as_str())
14003                .map(String::from),
14004            identity_validation_expression: props
14005                .get("IdentityValidationExpression")
14006                .and_then(|v| v.as_str())
14007                .map(String::from),
14008            authorizer_result_ttl_in_seconds: props
14009                .get("AuthorizerResultTtlInSeconds")
14010                .and_then(|v| v.as_i64())
14011                .map(|n| n as i32),
14012        };
14013
14014        let mut accounts = self.apigateway_state.write();
14015        let state = accounts.get_or_create(&self.account_id);
14016        if !state.apis.contains_key(&rest_api_id) {
14017            return Err(format!("RestApi {rest_api_id} not found"));
14018        }
14019        state
14020            .authorizers
14021            .entry(rest_api_id.clone())
14022            .or_default()
14023            .insert(id.clone(), auth);
14024
14025        Ok(ProvisionResult::new(id.clone())
14026            .with("AuthorizerId", id)
14027            .with("RestApiId", rest_api_id))
14028    }
14029
14030    fn delete_apigw_authorizer(
14031        &self,
14032        physical_id: &str,
14033        attributes: &BTreeMap<String, String>,
14034    ) -> Result<(), String> {
14035        let Some(rest_api_id) = attributes.get("RestApiId") else {
14036            return Ok(());
14037        };
14038        let mut accounts = self.apigateway_state.write();
14039        let state = accounts.get_or_create(&self.account_id);
14040        if let Some(map) = state.authorizers.get_mut(rest_api_id) {
14041            map.remove(physical_id);
14042        }
14043        Ok(())
14044    }
14045
14046    fn create_apigw_request_validator(
14047        &self,
14048        resource: &ResourceDefinition,
14049    ) -> Result<ProvisionResult, String> {
14050        let props = &resource.properties;
14051        let rest_api_id = props
14052            .get("RestApiId")
14053            .and_then(|v| v.as_str())
14054            .ok_or("RestApiId is required")?
14055            .to_string();
14056        let name = props.get("Name").and_then(|v| v.as_str()).map(String::from);
14057        let validate_body = props
14058            .get("ValidateRequestBody")
14059            .and_then(|v| v.as_bool())
14060            .unwrap_or(false);
14061        let validate_params = props
14062            .get("ValidateRequestParameters")
14063            .and_then(|v| v.as_bool())
14064            .unwrap_or(false);
14065        let id = apigw_make_id();
14066        let body = serde_json::json!({
14067            "id": id,
14068            "name": name,
14069            "validateRequestBody": validate_body,
14070            "validateRequestParameters": validate_params,
14071        });
14072        let mut accounts = self.apigateway_state.write();
14073        let state = accounts.get_or_create(&self.account_id);
14074        state
14075            .request_validators
14076            .entry(rest_api_id.clone())
14077            .or_default()
14078            .insert(id.clone(), body);
14079        Ok(ProvisionResult::new(id.clone())
14080            .with("RequestValidatorId", id)
14081            .with("RestApiId", rest_api_id))
14082    }
14083
14084    fn delete_apigw_request_validator(
14085        &self,
14086        physical_id: &str,
14087        attributes: &BTreeMap<String, String>,
14088    ) -> Result<(), String> {
14089        let Some(rest_api_id) = attributes.get("RestApiId") else {
14090            return Ok(());
14091        };
14092        let mut accounts = self.apigateway_state.write();
14093        let state = accounts.get_or_create(&self.account_id);
14094        if let Some(map) = state.request_validators.get_mut(rest_api_id) {
14095            map.remove(physical_id);
14096        }
14097        Ok(())
14098    }
14099
14100    fn create_apigw_model(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
14101        let props = &resource.properties;
14102        let rest_api_id = props
14103            .get("RestApiId")
14104            .and_then(|v| v.as_str())
14105            .ok_or("RestApiId is required")?
14106            .to_string();
14107        let name = props
14108            .get("Name")
14109            .and_then(|v| v.as_str())
14110            .ok_or("Name is required")?
14111            .to_string();
14112        let content_type = props
14113            .get("ContentType")
14114            .and_then(|v| v.as_str())
14115            .unwrap_or("application/json")
14116            .to_string();
14117        let schema = props.get("Schema").map(|v| {
14118            if let Some(s) = v.as_str() {
14119                s.to_string()
14120            } else {
14121                v.to_string()
14122            }
14123        });
14124        let id = apigw_make_id();
14125        let model = ApiGwModel {
14126            id: id.clone(),
14127            name: name.clone(),
14128            description: props
14129                .get("Description")
14130                .and_then(|v| v.as_str())
14131                .map(String::from),
14132            schema,
14133            content_type,
14134        };
14135        let mut accounts = self.apigateway_state.write();
14136        let state = accounts.get_or_create(&self.account_id);
14137        state
14138            .models
14139            .entry(rest_api_id.clone())
14140            .or_default()
14141            .insert(name.clone(), model);
14142        Ok(ProvisionResult::new(name.clone())
14143            .with("ModelName", name)
14144            .with("RestApiId", rest_api_id))
14145    }
14146
14147    fn delete_apigw_model(
14148        &self,
14149        physical_id: &str,
14150        attributes: &BTreeMap<String, String>,
14151    ) -> Result<(), String> {
14152        let Some(rest_api_id) = attributes.get("RestApiId") else {
14153            return Ok(());
14154        };
14155        let mut accounts = self.apigateway_state.write();
14156        let state = accounts.get_or_create(&self.account_id);
14157        if let Some(map) = state.models.get_mut(rest_api_id) {
14158            map.remove(physical_id);
14159        }
14160        Ok(())
14161    }
14162
14163    fn create_apigw_gateway_response(
14164        &self,
14165        resource: &ResourceDefinition,
14166    ) -> Result<ProvisionResult, String> {
14167        let props = &resource.properties;
14168        let rest_api_id = props
14169            .get("RestApiId")
14170            .and_then(|v| v.as_str())
14171            .ok_or("RestApiId is required")?
14172            .to_string();
14173        let response_type = props
14174            .get("ResponseType")
14175            .and_then(|v| v.as_str())
14176            .ok_or("ResponseType is required")?
14177            .to_string();
14178        let body = serde_json::json!({
14179            "responseType": response_type,
14180            "statusCode": props.get("StatusCode").and_then(|v| v.as_str()),
14181            "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
14182            "responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
14183        });
14184        let mut accounts = self.apigateway_state.write();
14185        let state = accounts.get_or_create(&self.account_id);
14186        state
14187            .gateway_responses
14188            .entry(rest_api_id.clone())
14189            .or_default()
14190            .insert(response_type.clone(), body);
14191        Ok(ProvisionResult::new(response_type.clone())
14192            .with("ResponseType", response_type)
14193            .with("RestApiId", rest_api_id))
14194    }
14195
14196    fn delete_apigw_gateway_response(
14197        &self,
14198        physical_id: &str,
14199        attributes: &BTreeMap<String, String>,
14200    ) -> Result<(), String> {
14201        let Some(rest_api_id) = attributes.get("RestApiId") else {
14202            return Ok(());
14203        };
14204        let mut accounts = self.apigateway_state.write();
14205        let state = accounts.get_or_create(&self.account_id);
14206        if let Some(map) = state.gateway_responses.get_mut(rest_api_id) {
14207            map.remove(physical_id);
14208        }
14209        Ok(())
14210    }
14211
14212    fn create_apigw_usage_plan(
14213        &self,
14214        resource: &ResourceDefinition,
14215    ) -> Result<ProvisionResult, String> {
14216        let props = &resource.properties;
14217        let name = props
14218            .get("UsagePlanName")
14219            .and_then(|v| v.as_str())
14220            .ok_or("UsagePlanName is required")?
14221            .to_string();
14222        let id = apigw_make_id();
14223        let plan = ApiGwUsagePlan {
14224            id: id.clone(),
14225            name,
14226            description: props
14227                .get("Description")
14228                .and_then(|v| v.as_str())
14229                .map(String::from),
14230            api_stages: props
14231                .get("ApiStages")
14232                .and_then(|v| v.as_array())
14233                .cloned()
14234                .unwrap_or_default()
14235                .into_iter()
14236                .map(lowercase_first_keys)
14237                .collect(),
14238            throttle: props.get("Throttle").cloned().map(lowercase_first_keys),
14239            quota: props.get("Quota").cloned().map(lowercase_first_keys),
14240            product_code: None,
14241            tags: parse_acm_tags(props.get("Tags")),
14242        };
14243        let mut accounts = self.apigateway_state.write();
14244        let state = accounts.get_or_create(&self.account_id);
14245        state.usage_plans.insert(id.clone(), plan);
14246        Ok(ProvisionResult::new(id.clone()).with("UsagePlanId", id))
14247    }
14248
14249    fn delete_apigw_usage_plan(&self, physical_id: &str) -> Result<(), String> {
14250        let mut accounts = self.apigateway_state.write();
14251        let state = accounts.get_or_create(&self.account_id);
14252        state.usage_plans.remove(physical_id);
14253        state.usage_plan_keys.remove(physical_id);
14254        Ok(())
14255    }
14256
14257    fn create_apigw_api_key(
14258        &self,
14259        resource: &ResourceDefinition,
14260    ) -> Result<ProvisionResult, String> {
14261        let props = &resource.properties;
14262        let generate_distinct_id = props
14263            .get("GenerateDistinctId")
14264            .and_then(|v| v.as_bool())
14265            .unwrap_or(false);
14266        let name = props
14267            .get("Name")
14268            .and_then(|v| v.as_str())
14269            .map(String::from)
14270            .unwrap_or_else(|| {
14271                if generate_distinct_id {
14272                    format!("cfn-key-{}-{}", resource.logical_id, apigw_make_id())
14273                } else {
14274                    format!("cfn-key-{}", resource.logical_id)
14275                }
14276            });
14277        let value = props
14278            .get("Value")
14279            .and_then(|v| v.as_str())
14280            .map(String::from)
14281            .unwrap_or_else(|| Uuid::new_v4().simple().to_string());
14282        let enabled = props
14283            .get("Enabled")
14284            .and_then(|v| v.as_bool())
14285            .unwrap_or(true);
14286        // CFN's StageKeys are `[{RestApiId, StageName}, ...]` — we store each
14287        // as `restApiId/stageName` per the live API key shape.
14288        let stage_keys: Vec<String> = props
14289            .get("StageKeys")
14290            .and_then(|v| v.as_array())
14291            .map(|arr| {
14292                arr.iter()
14293                    .filter_map(|s| {
14294                        let rest = s.get("RestApiId").and_then(|v| v.as_str())?;
14295                        let stage = s.get("StageName").and_then(|v| v.as_str())?;
14296                        Some(format!("{rest}/{stage}"))
14297                    })
14298                    .collect()
14299            })
14300            .unwrap_or_default();
14301        let id = apigw_make_id();
14302        let now = Utc::now();
14303        let key = ApiGwApiKey {
14304            id: id.clone(),
14305            value,
14306            name,
14307            description: props
14308                .get("Description")
14309                .and_then(|v| v.as_str())
14310                .map(String::from),
14311            enabled,
14312            created_date: now,
14313            last_updated_date: now,
14314            stage_keys,
14315            tags: parse_acm_tags(props.get("Tags")),
14316            customer_id: props
14317                .get("CustomerId")
14318                .and_then(|v| v.as_str())
14319                .map(String::from),
14320        };
14321        let mut accounts = self.apigateway_state.write();
14322        let state = accounts.get_or_create(&self.account_id);
14323        state.api_keys.insert(id.clone(), key);
14324        Ok(ProvisionResult::new(id.clone()).with("ApiKeyId", id))
14325    }
14326
14327    fn delete_apigw_api_key(&self, physical_id: &str) -> Result<(), String> {
14328        let mut accounts = self.apigateway_state.write();
14329        let state = accounts.get_or_create(&self.account_id);
14330        state.api_keys.remove(physical_id);
14331        Ok(())
14332    }
14333
14334    fn create_apigw_usage_plan_key(
14335        &self,
14336        resource: &ResourceDefinition,
14337    ) -> Result<ProvisionResult, String> {
14338        let props = &resource.properties;
14339        let usage_plan_id = props
14340            .get("UsagePlanId")
14341            .and_then(|v| v.as_str())
14342            .ok_or("UsagePlanId is required")?
14343            .to_string();
14344        let key_id = props
14345            .get("KeyId")
14346            .and_then(|v| v.as_str())
14347            .ok_or("KeyId is required")?
14348            .to_string();
14349        let key_type = props
14350            .get("KeyType")
14351            .and_then(|v| v.as_str())
14352            .unwrap_or("API_KEY")
14353            .to_string();
14354        let body = serde_json::json!({
14355            "id": key_id,
14356            "type": key_type,
14357        });
14358        let mut accounts = self.apigateway_state.write();
14359        let state = accounts.get_or_create(&self.account_id);
14360        if !state.usage_plans.contains_key(&usage_plan_id) {
14361            return Err(format!("UsagePlan {usage_plan_id} not yet provisioned"));
14362        }
14363        if !state.api_keys.contains_key(&key_id) {
14364            return Err(format!("ApiKey {key_id} not yet provisioned"));
14365        }
14366        state
14367            .usage_plan_keys
14368            .entry(usage_plan_id.clone())
14369            .or_default()
14370            .insert(key_id.clone(), body);
14371        let physical = format!("{usage_plan_id}/{key_id}");
14372        Ok(ProvisionResult::new(physical)
14373            .with("UsagePlanId", usage_plan_id)
14374            .with("KeyId", key_id))
14375    }
14376
14377    fn delete_apigw_usage_plan_key(
14378        &self,
14379        physical_id: &str,
14380        _attributes: &BTreeMap<String, String>,
14381    ) -> Result<(), String> {
14382        let mut parts = physical_id.splitn(2, '/');
14383        let Some(plan_id) = parts.next() else {
14384            return Ok(());
14385        };
14386        let Some(key_id) = parts.next() else {
14387            return Ok(());
14388        };
14389        let mut accounts = self.apigateway_state.write();
14390        let state = accounts.get_or_create(&self.account_id);
14391        if let Some(map) = state.usage_plan_keys.get_mut(plan_id) {
14392            map.remove(key_id);
14393        }
14394        Ok(())
14395    }
14396
14397    fn create_apigw_domain_name(
14398        &self,
14399        resource: &ResourceDefinition,
14400    ) -> Result<ProvisionResult, String> {
14401        let props = &resource.properties;
14402        let domain_name = props
14403            .get("DomainName")
14404            .and_then(|v| v.as_str())
14405            .ok_or("DomainName is required")?
14406            .to_string();
14407        let mtls = props
14408            .get("MutualTlsAuthentication")
14409            .cloned()
14410            .map(lowercase_first_keys);
14411        let regional_domain = format!(
14412            "d-{}.execute-api.{}.amazonaws.com",
14413            apigw_make_id(),
14414            self.region
14415        );
14416        let distribution_domain = format!("d{}.cloudfront.net", apigw_make_id());
14417        let body = serde_json::json!({
14418            "domainName": domain_name,
14419            "certificateArn": props.get("CertificateArn").and_then(|v| v.as_str()),
14420            "regionalCertificateArn": props.get("RegionalCertificateArn").and_then(|v| v.as_str()),
14421            "endpointConfiguration": props.get("EndpointConfiguration").cloned().unwrap_or(serde_json::json!({"types": ["EDGE"]})),
14422            "securityPolicy": props.get("SecurityPolicy").and_then(|v| v.as_str()),
14423            "ownershipVerificationCertificateArn": props.get("OwnershipVerificationCertificateArn").and_then(|v| v.as_str()),
14424            "regionalDomainName": regional_domain,
14425            "regionalHostedZoneId": "Z2FDTNDATAQYW2",
14426            "distributionDomainName": distribution_domain,
14427            "distributionHostedZoneId": "Z2FDTNDATAQYW2",
14428            "mutualTlsAuthentication": mtls,
14429            "tags": serde_json::Value::Object(
14430                parse_acm_tags(props.get("Tags"))
14431                    .into_iter()
14432                    .map(|(k, v)| (k, serde_json::Value::String(v)))
14433                    .collect(),
14434            ),
14435        });
14436        let mut accounts = self.apigateway_state.write();
14437        let state = accounts.get_or_create(&self.account_id);
14438        state.domain_names.insert(domain_name.clone(), body);
14439        Ok(ProvisionResult::new(domain_name.clone())
14440            .with("DomainName", domain_name)
14441            .with("RegionalHostedZoneId", "Z2FDTNDATAQYW2".to_string())
14442            .with("DistributionHostedZoneId", "Z2FDTNDATAQYW2".to_string()))
14443    }
14444
14445    fn delete_apigw_domain_name(&self, physical_id: &str) -> Result<(), String> {
14446        let mut accounts = self.apigateway_state.write();
14447        let state = accounts.get_or_create(&self.account_id);
14448        state.domain_names.remove(physical_id);
14449        state.base_path_mappings.remove(physical_id);
14450        Ok(())
14451    }
14452
14453    fn create_apigw_base_path_mapping(
14454        &self,
14455        resource: &ResourceDefinition,
14456    ) -> Result<ProvisionResult, String> {
14457        let props = &resource.properties;
14458        let domain_name = props
14459            .get("DomainName")
14460            .and_then(|v| v.as_str())
14461            .ok_or("DomainName is required")?
14462            .to_string();
14463        let rest_api_id = props
14464            .get("RestApiId")
14465            .and_then(|v| v.as_str())
14466            .ok_or("RestApiId is required")?
14467            .to_string();
14468        let base_path = props
14469            .get("BasePath")
14470            .and_then(|v| v.as_str())
14471            .unwrap_or("(none)")
14472            .to_string();
14473        let stage = props
14474            .get("Stage")
14475            .and_then(|v| v.as_str())
14476            .map(String::from);
14477        let body = serde_json::json!({
14478            "basePath": base_path,
14479            "restApiId": rest_api_id,
14480            "stage": stage,
14481        });
14482        let mut accounts = self.apigateway_state.write();
14483        let state = accounts.get_or_create(&self.account_id);
14484        state
14485            .base_path_mappings
14486            .entry(domain_name.clone())
14487            .or_default()
14488            .insert(base_path.clone(), body);
14489        let physical = format!("{domain_name}/{base_path}");
14490        Ok(ProvisionResult::new(physical)
14491            .with("DomainName", domain_name)
14492            .with("BasePath", base_path))
14493    }
14494
14495    fn delete_apigw_base_path_mapping(
14496        &self,
14497        physical_id: &str,
14498        _attributes: &BTreeMap<String, String>,
14499    ) -> Result<(), String> {
14500        let mut parts = physical_id.splitn(2, '/');
14501        let Some(domain) = parts.next() else {
14502            return Ok(());
14503        };
14504        let Some(base_path) = parts.next() else {
14505            return Ok(());
14506        };
14507        let mut accounts = self.apigateway_state.write();
14508        let state = accounts.get_or_create(&self.account_id);
14509        if let Some(map) = state.base_path_mappings.get_mut(domain) {
14510            map.remove(base_path);
14511        }
14512        Ok(())
14513    }
14514
14515    // --- API Gateway v1 update paths ---
14516    //
14517    // These mirror the create_* helpers above but mutate an existing
14518    // resource instead of inserting a new one. The physical id is
14519    // preserved across updates so other stack resources keep referencing
14520    // the same logical entity.
14521
14522    fn update_apigw_resource(
14523        &self,
14524        existing: &StackResource,
14525        resource: &ResourceDefinition,
14526    ) -> Result<ProvisionResult, String> {
14527        let props = &resource.properties;
14528        let rest_api_id = existing
14529            .attributes
14530            .get("RestApiId")
14531            .cloned()
14532            .or_else(|| {
14533                props
14534                    .get("RestApiId")
14535                    .and_then(|v| v.as_str())
14536                    .map(String::from)
14537            })
14538            .ok_or("RestApiId is required")?;
14539        let physical = existing.physical_id.clone();
14540        let mut accounts = self.apigateway_state.write();
14541        let state = accounts.get_or_create(&self.account_id);
14542        let api_resources = state
14543            .resources
14544            .get_mut(&rest_api_id)
14545            .ok_or_else(|| format!("RestApi {rest_api_id} not found"))?;
14546        if !api_resources.contains_key(&physical) {
14547            return Err(format!("Resource {physical} not found"));
14548        }
14549        if let Some(part) = props.get("PathPart").and_then(|v| v.as_str()) {
14550            // Read parent's path first (immutable borrow), then mutate.
14551            let parent_id = api_resources
14552                .get(&physical)
14553                .and_then(|r| r.parent_id.clone());
14554            let parent_path = parent_id
14555                .as_ref()
14556                .and_then(|pid| api_resources.get(pid).map(|p| p.path.clone()))
14557                .unwrap_or_else(|| "/".to_string());
14558            let new_path = if parent_path == "/" {
14559                format!("/{part}")
14560            } else {
14561                format!("{parent_path}/{part}")
14562            };
14563            let res = api_resources
14564                .get_mut(&physical)
14565                .expect("contains_key checked above");
14566            res.path_part = Some(part.to_string());
14567            res.path = new_path;
14568        }
14569        Ok(ProvisionResult::new(physical.clone())
14570            .with("ResourceId", physical)
14571            .with("RestApiId", rest_api_id))
14572    }
14573
14574    fn update_apigw_method(
14575        &self,
14576        existing: &StackResource,
14577        resource: &ResourceDefinition,
14578    ) -> Result<ProvisionResult, String> {
14579        // Method's physical id is the composite "rest/resource/method"
14580        // key. Identity props can't change without replacement, so we
14581        // simply rewrite the stored Method/Integration with current
14582        // properties. We delegate to create_apigw_method which already
14583        // handles the insert-or-replace semantics.
14584        self.create_apigw_method(resource).map(|r| {
14585            // Make sure the physical id stays stable.
14586            ProvisionResult {
14587                physical_id: existing.physical_id.clone(),
14588                attributes: r.attributes,
14589            }
14590        })
14591    }
14592
14593    fn update_apigw_deployment(
14594        &self,
14595        existing: &StackResource,
14596        resource: &ResourceDefinition,
14597    ) -> Result<ProvisionResult, String> {
14598        let props = &resource.properties;
14599        let rest_api_id = existing
14600            .attributes
14601            .get("RestApiId")
14602            .cloned()
14603            .or_else(|| {
14604                props
14605                    .get("RestApiId")
14606                    .and_then(|v| v.as_str())
14607                    .map(String::from)
14608            })
14609            .ok_or("RestApiId is required")?;
14610        let physical = existing.physical_id.clone();
14611        let mut accounts = self.apigateway_state.write();
14612        let state = accounts.get_or_create(&self.account_id);
14613        let dep = state
14614            .deployments
14615            .get_mut(&rest_api_id)
14616            .and_then(|m| m.get_mut(&physical))
14617            .ok_or_else(|| format!("Deployment {physical} not found"))?;
14618        if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
14619            dep.description = Some(desc.to_string());
14620        }
14621        Ok(ProvisionResult::new(physical.clone())
14622            .with("DeploymentId", physical)
14623            .with("RestApiId", rest_api_id))
14624    }
14625
14626    fn update_apigw_stage(
14627        &self,
14628        existing: &StackResource,
14629        resource: &ResourceDefinition,
14630    ) -> Result<ProvisionResult, String> {
14631        let props = &resource.properties;
14632        let rest_api_id = existing
14633            .attributes
14634            .get("RestApiId")
14635            .cloned()
14636            .or_else(|| {
14637                props
14638                    .get("RestApiId")
14639                    .and_then(|v| v.as_str())
14640                    .map(String::from)
14641            })
14642            .ok_or("RestApiId is required")?;
14643        let stage_name = existing.physical_id.clone();
14644        let mut accounts = self.apigateway_state.write();
14645        let state = accounts.get_or_create(&self.account_id);
14646        let stage = state
14647            .stages
14648            .get_mut(&rest_api_id)
14649            .and_then(|m| m.get_mut(&stage_name))
14650            .ok_or_else(|| format!("Stage {stage_name} not found"))?;
14651        if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
14652            stage.description = Some(desc.to_string());
14653        }
14654        if let Some(b) = props.get("TracingEnabled").and_then(|v| v.as_bool()) {
14655            stage.tracing_enabled = b;
14656        }
14657        if let Some(b) = props.get("CacheClusterEnabled").and_then(|v| v.as_bool()) {
14658            stage.cache_cluster_enabled = b;
14659        }
14660        if let Some(s) = props.get("CacheClusterSize").and_then(|v| v.as_str()) {
14661            stage.cache_cluster_size = Some(s.to_string());
14662        }
14663        if let Some(obj) = props.get("Variables").and_then(|v| v.as_object()) {
14664            stage.variables = obj
14665                .iter()
14666                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
14667                .collect();
14668        }
14669        if let Some(dep) = props.get("DeploymentId").and_then(|v| v.as_str()) {
14670            stage.deployment_id = dep.to_string();
14671        }
14672        if props.get("Tags").is_some() {
14673            stage.tags = parse_acm_tags(props.get("Tags"));
14674        }
14675        if let Some(arr) = props.get("MethodSettings").and_then(|v| v.as_array()) {
14676            stage.method_settings = arr
14677                .iter()
14678                .filter_map(|s| {
14679                    let path = s.get("ResourcePath").and_then(|v| v.as_str())?;
14680                    let http = s.get("HttpMethod").and_then(|v| v.as_str())?;
14681                    let key = format!("{}/{http}", path.strip_prefix('/').unwrap_or(path));
14682                    Some((key, s.clone()))
14683                })
14684                .collect();
14685        }
14686        if let Some(canary) = props.get("CanarySetting").cloned() {
14687            stage.canary_settings = Some(canary);
14688        }
14689        if let Some(access) = props.get("AccessLogSetting").cloned() {
14690            stage.access_log_settings = Some(access);
14691        }
14692        stage.last_updated_date = Utc::now();
14693        Ok(ProvisionResult::new(stage_name.clone())
14694            .with("StageName", stage_name)
14695            .with("RestApiId", rest_api_id))
14696    }
14697
14698    fn update_apigw_authorizer(
14699        &self,
14700        existing: &StackResource,
14701        resource: &ResourceDefinition,
14702    ) -> Result<ProvisionResult, String> {
14703        let props = &resource.properties;
14704        let rest_api_id = existing
14705            .attributes
14706            .get("RestApiId")
14707            .cloned()
14708            .or_else(|| {
14709                props
14710                    .get("RestApiId")
14711                    .and_then(|v| v.as_str())
14712                    .map(String::from)
14713            })
14714            .ok_or("RestApiId is required")?;
14715        let physical = existing.physical_id.clone();
14716        let mut accounts = self.apigateway_state.write();
14717        let state = accounts.get_or_create(&self.account_id);
14718        let auth = state
14719            .authorizers
14720            .get_mut(&rest_api_id)
14721            .and_then(|m| m.get_mut(&physical))
14722            .ok_or_else(|| format!("Authorizer {physical} not found"))?;
14723        if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
14724            auth.name = name.to_string();
14725        }
14726        if let Some(t) = props.get("Type").and_then(|v| v.as_str()) {
14727            auth.authorizer_type = t.to_string();
14728        }
14729        if let Some(uri) = props.get("AuthorizerUri").and_then(|v| v.as_str()) {
14730            auth.authorizer_uri = Some(uri.to_string());
14731        }
14732        if let Some(arr) = props.get("ProviderARNs").and_then(|v| v.as_array()) {
14733            auth.provider_arns = arr
14734                .iter()
14735                .filter_map(|v| v.as_str().map(String::from))
14736                .collect();
14737        }
14738        if let Some(s) = props.get("IdentitySource").and_then(|v| v.as_str()) {
14739            auth.identity_source = Some(s.to_string());
14740        }
14741        if let Some(s) = props
14742            .get("IdentityValidationExpression")
14743            .and_then(|v| v.as_str())
14744        {
14745            auth.identity_validation_expression = Some(s.to_string());
14746        }
14747        if let Some(n) = props
14748            .get("AuthorizerResultTtlInSeconds")
14749            .and_then(|v| v.as_i64())
14750        {
14751            auth.authorizer_result_ttl_in_seconds = Some(n as i32);
14752        }
14753        if let Some(s) = props.get("AuthType").and_then(|v| v.as_str()) {
14754            auth.auth_type = Some(s.to_string());
14755        }
14756        if let Some(s) = props.get("AuthorizerCredentials").and_then(|v| v.as_str()) {
14757            auth.authorizer_credentials = Some(s.to_string());
14758        }
14759        Ok(ProvisionResult::new(physical.clone())
14760            .with("AuthorizerId", physical)
14761            .with("RestApiId", rest_api_id))
14762    }
14763
14764    fn update_apigw_request_validator(
14765        &self,
14766        existing: &StackResource,
14767        resource: &ResourceDefinition,
14768    ) -> Result<ProvisionResult, String> {
14769        let props = &resource.properties;
14770        let rest_api_id = existing
14771            .attributes
14772            .get("RestApiId")
14773            .cloned()
14774            .or_else(|| {
14775                props
14776                    .get("RestApiId")
14777                    .and_then(|v| v.as_str())
14778                    .map(String::from)
14779            })
14780            .ok_or("RestApiId is required")?;
14781        let physical = existing.physical_id.clone();
14782        let mut accounts = self.apigateway_state.write();
14783        let state = accounts.get_or_create(&self.account_id);
14784        let body = state
14785            .request_validators
14786            .get_mut(&rest_api_id)
14787            .and_then(|m| m.get_mut(&physical))
14788            .ok_or_else(|| format!("RequestValidator {physical} not found"))?;
14789        let obj = body.as_object_mut().ok_or("validator body not object")?;
14790        if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
14791            obj.insert("name".into(), serde_json::Value::String(name.into()));
14792        }
14793        if let Some(b) = props.get("ValidateRequestBody").and_then(|v| v.as_bool()) {
14794            obj.insert("validateRequestBody".into(), serde_json::Value::Bool(b));
14795        }
14796        if let Some(b) = props
14797            .get("ValidateRequestParameters")
14798            .and_then(|v| v.as_bool())
14799        {
14800            obj.insert(
14801                "validateRequestParameters".into(),
14802                serde_json::Value::Bool(b),
14803            );
14804        }
14805        Ok(ProvisionResult::new(physical.clone())
14806            .with("RequestValidatorId", physical)
14807            .with("RestApiId", rest_api_id))
14808    }
14809
14810    fn update_apigw_model(
14811        &self,
14812        existing: &StackResource,
14813        resource: &ResourceDefinition,
14814    ) -> Result<ProvisionResult, String> {
14815        let props = &resource.properties;
14816        let rest_api_id = existing
14817            .attributes
14818            .get("RestApiId")
14819            .cloned()
14820            .or_else(|| {
14821                props
14822                    .get("RestApiId")
14823                    .and_then(|v| v.as_str())
14824                    .map(String::from)
14825            })
14826            .ok_or("RestApiId is required")?;
14827        let model_name = existing.physical_id.clone();
14828        let mut accounts = self.apigateway_state.write();
14829        let state = accounts.get_or_create(&self.account_id);
14830        let model = state
14831            .models
14832            .get_mut(&rest_api_id)
14833            .and_then(|m| m.get_mut(&model_name))
14834            .ok_or_else(|| format!("Model {model_name} not found"))?;
14835        if let Some(desc) = props.get("Description").and_then(|v| v.as_str()) {
14836            model.description = Some(desc.to_string());
14837        }
14838        if let Some(s) = props.get("ContentType").and_then(|v| v.as_str()) {
14839            model.content_type = s.to_string();
14840        }
14841        if let Some(schema) = props.get("Schema") {
14842            model.schema = Some(if let Some(s) = schema.as_str() {
14843                s.to_string()
14844            } else {
14845                schema.to_string()
14846            });
14847        }
14848        Ok(ProvisionResult::new(model_name.clone())
14849            .with("ModelName", model_name)
14850            .with("RestApiId", rest_api_id))
14851    }
14852
14853    fn update_apigw_gateway_response(
14854        &self,
14855        existing: &StackResource,
14856        resource: &ResourceDefinition,
14857    ) -> Result<ProvisionResult, String> {
14858        let props = &resource.properties;
14859        let rest_api_id = existing
14860            .attributes
14861            .get("RestApiId")
14862            .cloned()
14863            .or_else(|| {
14864                props
14865                    .get("RestApiId")
14866                    .and_then(|v| v.as_str())
14867                    .map(String::from)
14868            })
14869            .ok_or("RestApiId is required")?;
14870        let response_type = existing.physical_id.clone();
14871        let mut accounts = self.apigateway_state.write();
14872        let state = accounts.get_or_create(&self.account_id);
14873        let body = state
14874            .gateway_responses
14875            .get_mut(&rest_api_id)
14876            .and_then(|m| m.get_mut(&response_type))
14877            .ok_or_else(|| format!("GatewayResponse {response_type} not found"))?;
14878        let obj = body.as_object_mut().ok_or("response body not object")?;
14879        if let Some(s) = props.get("StatusCode").and_then(|v| v.as_str()) {
14880            obj.insert("statusCode".into(), serde_json::Value::String(s.into()));
14881        }
14882        if let Some(v) = props.get("ResponseParameters").cloned() {
14883            obj.insert("responseParameters".into(), v);
14884        }
14885        if let Some(v) = props.get("ResponseTemplates").cloned() {
14886            obj.insert("responseTemplates".into(), v);
14887        }
14888        Ok(ProvisionResult::new(response_type.clone())
14889            .with("ResponseType", response_type)
14890            .with("RestApiId", rest_api_id))
14891    }
14892
14893    fn update_apigw_usage_plan(
14894        &self,
14895        existing: &StackResource,
14896        resource: &ResourceDefinition,
14897    ) -> Result<ProvisionResult, String> {
14898        let props = &resource.properties;
14899        let physical = existing.physical_id.clone();
14900        let mut accounts = self.apigateway_state.write();
14901        let state = accounts.get_or_create(&self.account_id);
14902        let plan = state
14903            .usage_plans
14904            .get_mut(&physical)
14905            .ok_or_else(|| format!("UsagePlan {physical} not found"))?;
14906        if let Some(name) = props.get("UsagePlanName").and_then(|v| v.as_str()) {
14907            plan.name = name.to_string();
14908        }
14909        if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
14910            plan.description = Some(s.to_string());
14911        }
14912        if let Some(arr) = props.get("ApiStages").and_then(|v| v.as_array()) {
14913            plan.api_stages = arr.iter().cloned().map(lowercase_first_keys).collect();
14914        }
14915        if let Some(t) = props.get("Throttle").cloned() {
14916            plan.throttle = Some(lowercase_first_keys(t));
14917        }
14918        if let Some(q) = props.get("Quota").cloned() {
14919            plan.quota = Some(lowercase_first_keys(q));
14920        }
14921        if props.get("Tags").is_some() {
14922            plan.tags = parse_acm_tags(props.get("Tags"));
14923        }
14924        Ok(ProvisionResult::new(physical.clone()).with("UsagePlanId", physical))
14925    }
14926
14927    fn update_apigw_api_key(
14928        &self,
14929        existing: &StackResource,
14930        resource: &ResourceDefinition,
14931    ) -> Result<ProvisionResult, String> {
14932        let props = &resource.properties;
14933        let physical = existing.physical_id.clone();
14934        let mut accounts = self.apigateway_state.write();
14935        let state = accounts.get_or_create(&self.account_id);
14936        let key = state
14937            .api_keys
14938            .get_mut(&physical)
14939            .ok_or_else(|| format!("ApiKey {physical} not found"))?;
14940        if let Some(name) = props.get("Name").and_then(|v| v.as_str()) {
14941            key.name = name.to_string();
14942        }
14943        if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
14944            key.description = Some(s.to_string());
14945        }
14946        if let Some(b) = props.get("Enabled").and_then(|v| v.as_bool()) {
14947            key.enabled = b;
14948        }
14949        if let Some(s) = props.get("CustomerId").and_then(|v| v.as_str()) {
14950            key.customer_id = Some(s.to_string());
14951        }
14952        if props.get("Tags").is_some() {
14953            key.tags = parse_acm_tags(props.get("Tags"));
14954        }
14955        if let Some(arr) = props.get("StageKeys").and_then(|v| v.as_array()) {
14956            key.stage_keys = arr
14957                .iter()
14958                .filter_map(|s| {
14959                    let rest = s.get("RestApiId").and_then(|v| v.as_str())?;
14960                    let stage = s.get("StageName").and_then(|v| v.as_str())?;
14961                    Some(format!("{rest}/{stage}"))
14962                })
14963                .collect();
14964        }
14965        key.last_updated_date = Utc::now();
14966        Ok(ProvisionResult::new(physical.clone()).with("ApiKeyId", physical))
14967    }
14968
14969    fn update_apigw_usage_plan_key(
14970        &self,
14971        existing: &StackResource,
14972        _resource: &ResourceDefinition,
14973    ) -> Result<ProvisionResult, String> {
14974        // UsagePlanKey is a pure association (UsagePlan + ApiKey + Type) —
14975        // CFN treats every property as `requires-replacement`, so a real
14976        // UpdateStack would Delete+Create. Here we just preserve the
14977        // existing physical id so resolution stays stable.
14978        let physical = existing.physical_id.clone();
14979        let mut parts = physical.splitn(2, '/');
14980        let plan = parts.next().unwrap_or("").to_string();
14981        let key = parts.next().unwrap_or("").to_string();
14982        Ok(ProvisionResult::new(physical)
14983            .with("UsagePlanId", plan)
14984            .with("KeyId", key))
14985    }
14986
14987    fn update_apigw_domain_name(
14988        &self,
14989        existing: &StackResource,
14990        resource: &ResourceDefinition,
14991    ) -> Result<ProvisionResult, String> {
14992        let props = &resource.properties;
14993        let domain = existing.physical_id.clone();
14994        let mut accounts = self.apigateway_state.write();
14995        let state = accounts.get_or_create(&self.account_id);
14996        let body = state
14997            .domain_names
14998            .get_mut(&domain)
14999            .ok_or_else(|| format!("DomainName {domain} not found"))?;
15000        let obj = body.as_object_mut().ok_or("domain body not object")?;
15001        if let Some(s) = props.get("CertificateArn").and_then(|v| v.as_str()) {
15002            obj.insert("certificateArn".into(), serde_json::Value::String(s.into()));
15003        }
15004        if let Some(s) = props.get("RegionalCertificateArn").and_then(|v| v.as_str()) {
15005            obj.insert(
15006                "regionalCertificateArn".into(),
15007                serde_json::Value::String(s.into()),
15008            );
15009        }
15010        if let Some(v) = props.get("EndpointConfiguration").cloned() {
15011            obj.insert("endpointConfiguration".into(), v);
15012        }
15013        if let Some(s) = props.get("SecurityPolicy").and_then(|v| v.as_str()) {
15014            obj.insert("securityPolicy".into(), serde_json::Value::String(s.into()));
15015        }
15016        if let Some(v) = props.get("MutualTlsAuthentication").cloned() {
15017            obj.insert("mutualTlsAuthentication".into(), lowercase_first_keys(v));
15018        }
15019        if let Some(s) = props
15020            .get("OwnershipVerificationCertificateArn")
15021            .and_then(|v| v.as_str())
15022        {
15023            obj.insert(
15024                "ownershipVerificationCertificateArn".into(),
15025                serde_json::Value::String(s.into()),
15026            );
15027        }
15028        if props.get("Tags").is_some() {
15029            obj.insert(
15030                "tags".into(),
15031                serde_json::Value::Object(
15032                    parse_acm_tags(props.get("Tags"))
15033                        .into_iter()
15034                        .map(|(k, v)| (k, serde_json::Value::String(v)))
15035                        .collect(),
15036                ),
15037            );
15038        }
15039        Ok(ProvisionResult::new(domain.clone())
15040            .with("DomainName", domain)
15041            .with("RegionalHostedZoneId", "Z2FDTNDATAQYW2".to_string())
15042            .with("DistributionHostedZoneId", "Z2FDTNDATAQYW2".to_string()))
15043    }
15044
15045    fn update_apigw_base_path_mapping(
15046        &self,
15047        existing: &StackResource,
15048        resource: &ResourceDefinition,
15049    ) -> Result<ProvisionResult, String> {
15050        let props = &resource.properties;
15051        let physical = existing.physical_id.clone();
15052        let mut parts = physical.splitn(2, '/');
15053        let domain = parts
15054            .next()
15055            .ok_or("malformed base path mapping id")?
15056            .to_string();
15057        let base_path = parts
15058            .next()
15059            .ok_or("malformed base path mapping id")?
15060            .to_string();
15061        let mut accounts = self.apigateway_state.write();
15062        let state = accounts.get_or_create(&self.account_id);
15063        let map = state
15064            .base_path_mappings
15065            .get_mut(&domain)
15066            .ok_or_else(|| format!("DomainName {domain} not found"))?;
15067        let body = map
15068            .get_mut(&base_path)
15069            .ok_or_else(|| format!("BasePath {base_path} not found"))?;
15070        let obj = body.as_object_mut().ok_or("mapping body not object")?;
15071        if let Some(s) = props.get("RestApiId").and_then(|v| v.as_str()) {
15072            obj.insert("restApiId".into(), serde_json::Value::String(s.into()));
15073        }
15074        if let Some(s) = props.get("Stage").and_then(|v| v.as_str()) {
15075            obj.insert("stage".into(), serde_json::Value::String(s.into()));
15076        }
15077        Ok(ProvisionResult::new(physical)
15078            .with("DomainName", domain)
15079            .with("BasePath", base_path))
15080    }
15081
15082    // --- API Gateway v2 (HTTP/WebSocket APIs) ---
15083
15084    fn create_apigwv2_api(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
15085        let props = &resource.properties;
15086        let name = props
15087            .get("Name")
15088            .and_then(|v| v.as_str())
15089            .ok_or("Name is required")?
15090            .to_string();
15091        let protocol_type = props
15092            .get("ProtocolType")
15093            .and_then(|v| v.as_str())
15094            .unwrap_or("HTTP")
15095            .to_string();
15096        let description = props
15097            .get("Description")
15098            .and_then(|v| v.as_str())
15099            .map(String::from);
15100        let tags: Option<BTreeMap<String, String>> =
15101            props.get("Tags").and_then(|v| v.as_object()).map(|obj| {
15102                obj.iter()
15103                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
15104                    .collect()
15105            });
15106
15107        let id = make_apigwv2_id(10);
15108        let mut api = ApiGwV2HttpApi::new(id.clone(), name, description, tags, &self.region);
15109        api.protocol_type = protocol_type.clone();
15110        if let Some(expr) = props
15111            .get("RouteSelectionExpression")
15112            .and_then(|v| v.as_str())
15113        {
15114            api.route_selection_expression = expr.to_string();
15115        }
15116        if let Some(expr) = props
15117            .get("ApiKeySelectionExpression")
15118            .and_then(|v| v.as_str())
15119        {
15120            api.api_key_selection_expression = expr.to_string();
15121        }
15122        if let Some(b) = props
15123            .get("DisableExecuteApiEndpoint")
15124            .and_then(|v| v.as_bool())
15125        {
15126            api.disable_execute_api_endpoint = b;
15127        }
15128        if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
15129            api.ip_address_type = s.to_string();
15130        }
15131        if let Some(cors) = props.get("CorsConfiguration").and_then(|v| v.as_object()) {
15132            api.cors_configuration = Some(ApiGwV2CorsConfiguration {
15133                allow_credentials: cors.get("AllowCredentials").and_then(|v| v.as_bool()),
15134                allow_headers: cors
15135                    .get("AllowHeaders")
15136                    .and_then(|v| v.as_array())
15137                    .map(|a| {
15138                        a.iter()
15139                            .filter_map(|v| v.as_str().map(String::from))
15140                            .collect()
15141                    }),
15142                allow_methods: cors
15143                    .get("AllowMethods")
15144                    .and_then(|v| v.as_array())
15145                    .map(|a| {
15146                        a.iter()
15147                            .filter_map(|v| v.as_str().map(String::from))
15148                            .collect()
15149                    }),
15150                allow_origins: cors
15151                    .get("AllowOrigins")
15152                    .and_then(|v| v.as_array())
15153                    .map(|a| {
15154                        a.iter()
15155                            .filter_map(|v| v.as_str().map(String::from))
15156                            .collect()
15157                    }),
15158                expose_headers: cors
15159                    .get("ExposeHeaders")
15160                    .and_then(|v| v.as_array())
15161                    .map(|a| {
15162                        a.iter()
15163                            .filter_map(|v| v.as_str().map(String::from))
15164                            .collect()
15165                    }),
15166                max_age: cors
15167                    .get("MaxAge")
15168                    .and_then(|v| v.as_i64())
15169                    .map(|n| n as i32),
15170            });
15171        }
15172
15173        let api_endpoint = api.api_endpoint.clone();
15174        let mut accounts = self.apigatewayv2_state.write();
15175        let state = accounts.get_or_create(&self.account_id);
15176        state.apis.insert(id.clone(), api);
15177
15178        Ok(ProvisionResult::new(id.clone())
15179            .with("ApiId", id)
15180            .with("ApiEndpoint", api_endpoint))
15181    }
15182
15183    fn delete_apigwv2_api(&self, physical_id: &str) -> Result<(), String> {
15184        let mut accounts = self.apigatewayv2_state.write();
15185        let state = accounts.get_or_create(&self.account_id);
15186        state.apis.remove(physical_id);
15187        state.routes.remove(physical_id);
15188        state.integrations.remove(physical_id);
15189        state.stages.remove(physical_id);
15190        state.deployments.remove(physical_id);
15191        state.authorizers.remove(physical_id);
15192        state.models.remove(physical_id);
15193        state.integration_responses.remove(physical_id);
15194        state.route_responses.remove(physical_id);
15195        Ok(())
15196    }
15197
15198    fn create_apigwv2_route(
15199        &self,
15200        resource: &ResourceDefinition,
15201    ) -> Result<ProvisionResult, String> {
15202        let props = &resource.properties;
15203        let api_id = props
15204            .get("ApiId")
15205            .and_then(|v| v.as_str())
15206            .ok_or("ApiId is required")?
15207            .to_string();
15208        let route_key = props
15209            .get("RouteKey")
15210            .and_then(|v| v.as_str())
15211            .ok_or("RouteKey is required")?
15212            .to_string();
15213
15214        let mut accounts = self.apigatewayv2_state.write();
15215        let state = accounts.get_or_create(&self.account_id);
15216        if !state.apis.contains_key(&api_id) {
15217            return Err(format!("Api {api_id} not yet provisioned"));
15218        }
15219        let id = make_apigwv2_id(10);
15220        let route = ApiGwV2Route {
15221            route_id: id.clone(),
15222            route_key,
15223            target: props
15224                .get("Target")
15225                .and_then(|v| v.as_str())
15226                .map(String::from),
15227            authorization_type: props
15228                .get("AuthorizationType")
15229                .and_then(|v| v.as_str())
15230                .map(String::from),
15231            authorizer_id: props
15232                .get("AuthorizerId")
15233                .and_then(|v| v.as_str())
15234                .map(String::from),
15235        };
15236        state
15237            .routes
15238            .entry(api_id.clone())
15239            .or_default()
15240            .insert(id.clone(), route);
15241
15242        Ok(ProvisionResult::new(id.clone())
15243            .with("RouteId", id)
15244            .with("ApiId", api_id))
15245    }
15246
15247    fn delete_apigwv2_route(
15248        &self,
15249        physical_id: &str,
15250        attributes: &BTreeMap<String, String>,
15251    ) -> Result<(), String> {
15252        let Some(api_id) = attributes.get("ApiId") else {
15253            return Ok(());
15254        };
15255        let mut accounts = self.apigatewayv2_state.write();
15256        let state = accounts.get_or_create(&self.account_id);
15257        if let Some(map) = state.routes.get_mut(api_id) {
15258            map.remove(physical_id);
15259        }
15260        Ok(())
15261    }
15262
15263    fn create_apigwv2_integration(
15264        &self,
15265        resource: &ResourceDefinition,
15266    ) -> Result<ProvisionResult, String> {
15267        let props = &resource.properties;
15268        let api_id = props
15269            .get("ApiId")
15270            .and_then(|v| v.as_str())
15271            .ok_or("ApiId is required")?
15272            .to_string();
15273        let integration_type = props
15274            .get("IntegrationType")
15275            .and_then(|v| v.as_str())
15276            .ok_or("IntegrationType is required")?
15277            .to_string();
15278
15279        let mut accounts = self.apigatewayv2_state.write();
15280        let state = accounts.get_or_create(&self.account_id);
15281        if !state.apis.contains_key(&api_id) {
15282            return Err(format!("Api {api_id} not yet provisioned"));
15283        }
15284        let id = make_apigwv2_id(10);
15285        let integration = ApiGwV2Integration {
15286            integration_id: id.clone(),
15287            integration_type,
15288            integration_uri: props
15289                .get("IntegrationUri")
15290                .and_then(|v| v.as_str())
15291                .map(String::from),
15292            payload_format_version: props
15293                .get("PayloadFormatVersion")
15294                .and_then(|v| v.as_str())
15295                .map(String::from),
15296            timeout_in_millis: props.get("TimeoutInMillis").and_then(|v| v.as_i64()),
15297        };
15298        state
15299            .integrations
15300            .entry(api_id.clone())
15301            .or_default()
15302            .insert(id.clone(), integration);
15303
15304        Ok(ProvisionResult::new(id.clone())
15305            .with("IntegrationId", id)
15306            .with("ApiId", api_id))
15307    }
15308
15309    fn delete_apigwv2_integration(
15310        &self,
15311        physical_id: &str,
15312        attributes: &BTreeMap<String, String>,
15313    ) -> Result<(), String> {
15314        let Some(api_id) = attributes.get("ApiId") else {
15315            return Ok(());
15316        };
15317        let mut accounts = self.apigatewayv2_state.write();
15318        let state = accounts.get_or_create(&self.account_id);
15319        if let Some(map) = state.integrations.get_mut(api_id) {
15320            map.remove(physical_id);
15321        }
15322        Ok(())
15323    }
15324
15325    fn create_apigwv2_integration_response(
15326        &self,
15327        resource: &ResourceDefinition,
15328    ) -> Result<ProvisionResult, String> {
15329        let props = &resource.properties;
15330        let api_id = props
15331            .get("ApiId")
15332            .and_then(|v| v.as_str())
15333            .ok_or("ApiId is required")?
15334            .to_string();
15335        let integration_id = props
15336            .get("IntegrationId")
15337            .and_then(|v| v.as_str())
15338            .ok_or("IntegrationId is required")?
15339            .to_string();
15340        let key_expr = props
15341            .get("IntegrationResponseKey")
15342            .and_then(|v| v.as_str())
15343            .ok_or("IntegrationResponseKey is required")?
15344            .to_string();
15345        let id = make_apigwv2_id(10);
15346        let body = serde_json::json!({
15347            "integrationResponseId": id,
15348            "integrationId": integration_id,
15349            "integrationResponseKey": key_expr,
15350            "responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
15351            "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
15352            "templateSelectionExpression": props.get("TemplateSelectionExpression").and_then(|v| v.as_str()),
15353            "contentHandlingStrategy": props.get("ContentHandlingStrategy").and_then(|v| v.as_str()),
15354        });
15355        let composite_key = format!("{integration_id}/{id}");
15356        let mut accounts = self.apigatewayv2_state.write();
15357        let state = accounts.get_or_create(&self.account_id);
15358        if !state
15359            .integrations
15360            .get(&api_id)
15361            .map(|m| m.contains_key(&integration_id))
15362            .unwrap_or(false)
15363        {
15364            return Err(format!(
15365                "Integration {integration_id} not yet provisioned for api {api_id}"
15366            ));
15367        }
15368        state
15369            .integration_responses
15370            .entry(api_id.clone())
15371            .or_default()
15372            .insert(composite_key.clone(), body);
15373        Ok(ProvisionResult::new(composite_key.clone())
15374            .with("IntegrationResponseId", id)
15375            .with("IntegrationId", integration_id)
15376            .with("ApiId", api_id))
15377    }
15378
15379    fn delete_apigwv2_integration_response(
15380        &self,
15381        physical_id: &str,
15382        attributes: &BTreeMap<String, String>,
15383    ) -> Result<(), String> {
15384        let Some(api_id) = attributes.get("ApiId") else {
15385            return Ok(());
15386        };
15387        let mut accounts = self.apigatewayv2_state.write();
15388        let state = accounts.get_or_create(&self.account_id);
15389        if let Some(map) = state.integration_responses.get_mut(api_id) {
15390            map.remove(physical_id);
15391        }
15392        Ok(())
15393    }
15394
15395    fn create_apigwv2_route_response(
15396        &self,
15397        resource: &ResourceDefinition,
15398    ) -> Result<ProvisionResult, String> {
15399        let props = &resource.properties;
15400        let api_id = props
15401            .get("ApiId")
15402            .and_then(|v| v.as_str())
15403            .ok_or("ApiId is required")?
15404            .to_string();
15405        let route_id = props
15406            .get("RouteId")
15407            .and_then(|v| v.as_str())
15408            .ok_or("RouteId is required")?
15409            .to_string();
15410        let key_expr = props
15411            .get("RouteResponseKey")
15412            .and_then(|v| v.as_str())
15413            .ok_or("RouteResponseKey is required")?
15414            .to_string();
15415        let id = make_apigwv2_id(10);
15416        let body = serde_json::json!({
15417            "routeResponseId": id,
15418            "routeId": route_id,
15419            "routeResponseKey": key_expr,
15420            "responseModels": props.get("ResponseModels").cloned().unwrap_or(serde_json::json!({})),
15421            "modelSelectionExpression": props.get("ModelSelectionExpression").and_then(|v| v.as_str()),
15422            "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
15423        });
15424        let composite = format!("{route_id}/{id}");
15425        let mut accounts = self.apigatewayv2_state.write();
15426        let state = accounts.get_or_create(&self.account_id);
15427        if !state
15428            .routes
15429            .get(&api_id)
15430            .map(|m| m.contains_key(&route_id))
15431            .unwrap_or(false)
15432        {
15433            return Err(format!(
15434                "Route {route_id} not yet provisioned for api {api_id}"
15435            ));
15436        }
15437        state
15438            .route_responses
15439            .entry(api_id.clone())
15440            .or_default()
15441            .insert(composite.clone(), body);
15442        Ok(ProvisionResult::new(composite.clone())
15443            .with("RouteResponseId", id)
15444            .with("RouteId", route_id)
15445            .with("ApiId", api_id))
15446    }
15447
15448    fn delete_apigwv2_route_response(
15449        &self,
15450        physical_id: &str,
15451        attributes: &BTreeMap<String, String>,
15452    ) -> Result<(), String> {
15453        let Some(api_id) = attributes.get("ApiId") else {
15454            return Ok(());
15455        };
15456        let mut accounts = self.apigatewayv2_state.write();
15457        let state = accounts.get_or_create(&self.account_id);
15458        if let Some(map) = state.route_responses.get_mut(api_id) {
15459            map.remove(physical_id);
15460        }
15461        Ok(())
15462    }
15463
15464    fn create_apigwv2_stage(
15465        &self,
15466        resource: &ResourceDefinition,
15467    ) -> Result<ProvisionResult, String> {
15468        let props = &resource.properties;
15469        let api_id = props
15470            .get("ApiId")
15471            .and_then(|v| v.as_str())
15472            .ok_or("ApiId is required")?
15473            .to_string();
15474        let stage_name = props
15475            .get("StageName")
15476            .and_then(|v| v.as_str())
15477            .ok_or("StageName is required")?
15478            .to_string();
15479        let auto_deploy = props
15480            .get("AutoDeploy")
15481            .and_then(|v| v.as_bool())
15482            .unwrap_or(false);
15483        let deployment_id = props
15484            .get("DeploymentId")
15485            .and_then(|v| v.as_str())
15486            .map(String::from);
15487
15488        let stage_variables = props
15489            .get("StageVariables")
15490            .and_then(|v| v.as_object())
15491            .map(|obj| {
15492                obj.iter()
15493                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
15494                    .collect()
15495            });
15496
15497        let access_log_settings = props.get("AccessLogSettings").and_then(|v| {
15498            let destination_arn = v.get("DestinationArn")?.as_str()?.to_string();
15499            let format = v.get("Format").and_then(|f| f.as_str().map(String::from));
15500            Some(fakecloud_apigatewayv2::AccessLogSettings {
15501                destination_arn,
15502                format,
15503            })
15504        });
15505
15506        let stage = ApiGwV2Stage {
15507            stage_name: stage_name.clone(),
15508            description: props
15509                .get("Description")
15510                .and_then(|v| v.as_str())
15511                .map(String::from),
15512            deployment_id: deployment_id.clone(),
15513            auto_deploy,
15514            created_date: Utc::now(),
15515            last_updated_date: None,
15516            web_acl_arn: None,
15517            stage_variables,
15518            access_log_settings,
15519        };
15520
15521        let mut accounts = self.apigatewayv2_state.write();
15522        let state = accounts.get_or_create(&self.account_id);
15523        if !state.apis.contains_key(&api_id) {
15524            return Err(format!("Api {api_id} not yet provisioned"));
15525        }
15526        if let Some(dep) = &deployment_id {
15527            if !state
15528                .deployments
15529                .get(&api_id)
15530                .map(|m| m.contains_key(dep))
15531                .unwrap_or(false)
15532            {
15533                return Err(format!(
15534                    "Deployment {dep} not yet provisioned for api {api_id}"
15535                ));
15536            }
15537        }
15538        state
15539            .stages
15540            .entry(api_id.clone())
15541            .or_default()
15542            .insert(stage_name.clone(), stage);
15543
15544        Ok(ProvisionResult::new(stage_name.clone())
15545            .with("StageName", stage_name)
15546            .with("ApiId", api_id))
15547    }
15548
15549    fn delete_apigwv2_stage(
15550        &self,
15551        physical_id: &str,
15552        attributes: &BTreeMap<String, String>,
15553    ) -> Result<(), String> {
15554        let Some(api_id) = attributes.get("ApiId") else {
15555            return Ok(());
15556        };
15557        let mut accounts = self.apigatewayv2_state.write();
15558        let state = accounts.get_or_create(&self.account_id);
15559        if let Some(map) = state.stages.get_mut(api_id) {
15560            map.remove(physical_id);
15561        }
15562        Ok(())
15563    }
15564
15565    fn create_apigwv2_deployment(
15566        &self,
15567        resource: &ResourceDefinition,
15568    ) -> Result<ProvisionResult, String> {
15569        let props = &resource.properties;
15570        let api_id = props
15571            .get("ApiId")
15572            .and_then(|v| v.as_str())
15573            .ok_or("ApiId is required")?
15574            .to_string();
15575        let id = make_apigwv2_id(10);
15576        let deployment = ApiGwV2Deployment {
15577            deployment_id: id.clone(),
15578            description: props
15579                .get("Description")
15580                .and_then(|v| v.as_str())
15581                .map(String::from),
15582            created_date: Utc::now(),
15583            auto_deployed: false,
15584        };
15585        let mut accounts = self.apigatewayv2_state.write();
15586        let state = accounts.get_or_create(&self.account_id);
15587        if !state.apis.contains_key(&api_id) {
15588            return Err(format!("Api {api_id} not yet provisioned"));
15589        }
15590        state
15591            .deployments
15592            .entry(api_id.clone())
15593            .or_default()
15594            .insert(id.clone(), deployment);
15595        Ok(ProvisionResult::new(id.clone())
15596            .with("DeploymentId", id)
15597            .with("ApiId", api_id))
15598    }
15599
15600    fn delete_apigwv2_deployment(
15601        &self,
15602        physical_id: &str,
15603        attributes: &BTreeMap<String, String>,
15604    ) -> Result<(), String> {
15605        let Some(api_id) = attributes.get("ApiId") else {
15606            return Ok(());
15607        };
15608        let mut accounts = self.apigatewayv2_state.write();
15609        let state = accounts.get_or_create(&self.account_id);
15610        if let Some(map) = state.deployments.get_mut(api_id) {
15611            map.remove(physical_id);
15612        }
15613        Ok(())
15614    }
15615
15616    fn create_apigwv2_authorizer(
15617        &self,
15618        resource: &ResourceDefinition,
15619    ) -> Result<ProvisionResult, String> {
15620        let props = &resource.properties;
15621        let api_id = props
15622            .get("ApiId")
15623            .and_then(|v| v.as_str())
15624            .ok_or("ApiId is required")?
15625            .to_string();
15626        let name = props
15627            .get("Name")
15628            .and_then(|v| v.as_str())
15629            .ok_or("Name is required")?
15630            .to_string();
15631        let authorizer_type = props
15632            .get("AuthorizerType")
15633            .and_then(|v| v.as_str())
15634            .unwrap_or("REQUEST")
15635            .to_string();
15636        let identity_source = props
15637            .get("IdentitySource")
15638            .and_then(|v| v.as_array())
15639            .map(|arr| {
15640                arr.iter()
15641                    .filter_map(|v| v.as_str().map(String::from))
15642                    .collect::<Vec<String>>()
15643            });
15644        let jwt_configuration = props
15645            .get("JwtConfiguration")
15646            .and_then(|v| v.as_object())
15647            .map(|obj| ApiGwV2JwtConfiguration {
15648                audience: obj.get("Audience").and_then(|v| v.as_array()).map(|a| {
15649                    a.iter()
15650                        .filter_map(|v| v.as_str().map(String::from))
15651                        .collect()
15652                }),
15653                issuer: obj.get("Issuer").and_then(|v| v.as_str()).map(String::from),
15654            });
15655
15656        let id = make_apigwv2_id(10);
15657        let auth = ApiGwV2Authorizer {
15658            authorizer_id: id.clone(),
15659            name,
15660            authorizer_type,
15661            authorizer_uri: props
15662                .get("AuthorizerUri")
15663                .and_then(|v| v.as_str())
15664                .map(String::from),
15665            identity_source,
15666            jwt_configuration,
15667        };
15668        let mut accounts = self.apigatewayv2_state.write();
15669        let state = accounts.get_or_create(&self.account_id);
15670        if !state.apis.contains_key(&api_id) {
15671            return Err(format!("Api {api_id} not yet provisioned"));
15672        }
15673        state
15674            .authorizers
15675            .entry(api_id.clone())
15676            .or_default()
15677            .insert(id.clone(), auth);
15678        Ok(ProvisionResult::new(id.clone())
15679            .with("AuthorizerId", id)
15680            .with("ApiId", api_id))
15681    }
15682
15683    fn delete_apigwv2_authorizer(
15684        &self,
15685        physical_id: &str,
15686        attributes: &BTreeMap<String, String>,
15687    ) -> Result<(), String> {
15688        let Some(api_id) = attributes.get("ApiId") else {
15689            return Ok(());
15690        };
15691        let mut accounts = self.apigatewayv2_state.write();
15692        let state = accounts.get_or_create(&self.account_id);
15693        if let Some(map) = state.authorizers.get_mut(api_id) {
15694            map.remove(physical_id);
15695        }
15696        Ok(())
15697    }
15698
15699    fn create_apigwv2_domain_name(
15700        &self,
15701        resource: &ResourceDefinition,
15702    ) -> Result<ProvisionResult, String> {
15703        let props = &resource.properties;
15704        let domain_name = props
15705            .get("DomainName")
15706            .and_then(|v| v.as_str())
15707            .ok_or("DomainName is required")?
15708            .to_string();
15709        let body = serde_json::json!({
15710            "domainName": domain_name,
15711            "domainNameConfigurations": props.get("DomainNameConfigurations").cloned().unwrap_or(serde_json::json!([])),
15712            "mutualTlsAuthentication": props.get("MutualTlsAuthentication").cloned(),
15713            "apiMappingSelectionExpression": null,
15714        });
15715        let mut accounts = self.apigatewayv2_state.write();
15716        let state = accounts.get_or_create(&self.account_id);
15717        state.domain_names.insert(domain_name.clone(), body);
15718        Ok(ProvisionResult::new(domain_name.clone()).with("DomainName", domain_name))
15719    }
15720
15721    fn delete_apigwv2_domain_name(&self, physical_id: &str) -> Result<(), String> {
15722        let mut accounts = self.apigatewayv2_state.write();
15723        let state = accounts.get_or_create(&self.account_id);
15724        state.domain_names.remove(physical_id);
15725        state.api_mappings.remove(physical_id);
15726        Ok(())
15727    }
15728
15729    fn create_apigwv2_api_mapping(
15730        &self,
15731        resource: &ResourceDefinition,
15732    ) -> Result<ProvisionResult, String> {
15733        let props = &resource.properties;
15734        let domain_name = props
15735            .get("DomainName")
15736            .and_then(|v| v.as_str())
15737            .ok_or("DomainName is required")?
15738            .to_string();
15739        let api_id = props
15740            .get("ApiId")
15741            .and_then(|v| v.as_str())
15742            .ok_or("ApiId is required")?
15743            .to_string();
15744        let stage = props
15745            .get("Stage")
15746            .and_then(|v| v.as_str())
15747            .ok_or("Stage is required")?
15748            .to_string();
15749        let api_mapping_key = props
15750            .get("ApiMappingKey")
15751            .and_then(|v| v.as_str())
15752            .map(String::from);
15753        let id = make_apigwv2_id(10);
15754        let body = serde_json::json!({
15755            "apiMappingId": id,
15756            "apiId": api_id,
15757            "stage": stage,
15758            "apiMappingKey": api_mapping_key,
15759        });
15760        let mut accounts = self.apigatewayv2_state.write();
15761        let state = accounts.get_or_create(&self.account_id);
15762        if !state.domain_names.contains_key(&domain_name) {
15763            return Err(format!("DomainName {domain_name} not yet provisioned"));
15764        }
15765        if !state.apis.contains_key(&api_id) {
15766            return Err(format!("Api {api_id} not yet provisioned"));
15767        }
15768        state
15769            .api_mappings
15770            .entry(domain_name.clone())
15771            .or_default()
15772            .insert(id.clone(), body);
15773        Ok(ProvisionResult::new(id.clone())
15774            .with("ApiMappingId", id)
15775            .with("DomainName", domain_name))
15776    }
15777
15778    fn delete_apigwv2_api_mapping(
15779        &self,
15780        physical_id: &str,
15781        attributes: &BTreeMap<String, String>,
15782    ) -> Result<(), String> {
15783        let Some(domain) = attributes.get("DomainName") else {
15784            return Ok(());
15785        };
15786        let mut accounts = self.apigatewayv2_state.write();
15787        let state = accounts.get_or_create(&self.account_id);
15788        if let Some(map) = state.api_mappings.get_mut(domain) {
15789            map.remove(physical_id);
15790        }
15791        Ok(())
15792    }
15793
15794    fn create_apigwv2_vpc_link(
15795        &self,
15796        resource: &ResourceDefinition,
15797    ) -> Result<ProvisionResult, String> {
15798        let props = &resource.properties;
15799        let name = props
15800            .get("Name")
15801            .and_then(|v| v.as_str())
15802            .ok_or("Name is required")?
15803            .to_string();
15804        let id = make_apigwv2_id(10);
15805        let body = serde_json::json!({
15806            "vpcLinkId": id,
15807            "name": name,
15808            "subnetIds": props.get("SubnetIds").cloned().unwrap_or(serde_json::json!([])),
15809            "securityGroupIds": props.get("SecurityGroupIds").cloned().unwrap_or(serde_json::json!([])),
15810            "tags": props.get("Tags").cloned().unwrap_or(serde_json::json!({})),
15811            "vpcLinkStatus": "AVAILABLE",
15812            "vpcLinkVersion": "V2",
15813            "createdDate": Utc::now().to_rfc3339(),
15814        });
15815        let mut accounts = self.apigatewayv2_state.write();
15816        let state = accounts.get_or_create(&self.account_id);
15817        state.vpc_links.insert(id.clone(), body);
15818        Ok(ProvisionResult::new(id.clone()).with("VpcLinkId", id))
15819    }
15820
15821    fn delete_apigwv2_vpc_link(&self, physical_id: &str) -> Result<(), String> {
15822        let mut accounts = self.apigatewayv2_state.write();
15823        let state = accounts.get_or_create(&self.account_id);
15824        state.vpc_links.remove(physical_id);
15825        Ok(())
15826    }
15827
15828    fn create_apigwv2_model(
15829        &self,
15830        resource: &ResourceDefinition,
15831    ) -> Result<ProvisionResult, String> {
15832        let props = &resource.properties;
15833        let api_id = props
15834            .get("ApiId")
15835            .and_then(|v| v.as_str())
15836            .ok_or("ApiId is required")?
15837            .to_string();
15838        let name = props
15839            .get("Name")
15840            .and_then(|v| v.as_str())
15841            .ok_or("Name is required")?
15842            .to_string();
15843        let id = make_apigwv2_id(10);
15844        let body = serde_json::json!({
15845            "modelId": id,
15846            "name": name,
15847            "contentType": props.get("ContentType").and_then(|v| v.as_str()).unwrap_or("application/json"),
15848            "description": props.get("Description").and_then(|v| v.as_str()),
15849            "schema": props.get("Schema").map(|v| if let Some(s) = v.as_str() { s.to_string() } else { v.to_string() }),
15850        });
15851        let mut accounts = self.apigatewayv2_state.write();
15852        let state = accounts.get_or_create(&self.account_id);
15853        if !state.apis.contains_key(&api_id) {
15854            return Err(format!("Api {api_id} not yet provisioned"));
15855        }
15856        state
15857            .models
15858            .entry(api_id.clone())
15859            .or_default()
15860            .insert(id.clone(), body);
15861        Ok(ProvisionResult::new(id.clone())
15862            .with("ModelId", id)
15863            .with("ApiId", api_id))
15864    }
15865
15866    fn delete_apigwv2_model(
15867        &self,
15868        physical_id: &str,
15869        attributes: &BTreeMap<String, String>,
15870    ) -> Result<(), String> {
15871        let Some(api_id) = attributes.get("ApiId") else {
15872            return Ok(());
15873        };
15874        let mut accounts = self.apigatewayv2_state.write();
15875        let state = accounts.get_or_create(&self.account_id);
15876        if let Some(map) = state.models.get_mut(api_id) {
15877            map.remove(physical_id);
15878        }
15879        Ok(())
15880    }
15881
15882    /// In-place update for AWS::ApiGatewayV2::Api. CFN keeps the same
15883    /// ApiId across updates; only mutable properties (Name, Description,
15884    /// CORS, RouteSelectionExpression, etc.) are rewritten.
15885    fn update_apigwv2_api(
15886        &self,
15887        existing: &StackResource,
15888        resource: &ResourceDefinition,
15889    ) -> Result<ProvisionResult, String> {
15890        let props = &resource.properties;
15891        let api_id = existing.physical_id.clone();
15892        let mut accounts = self.apigatewayv2_state.write();
15893        let state = accounts.get_or_create(&self.account_id);
15894        let api = state
15895            .apis
15896            .get_mut(&api_id)
15897            .ok_or_else(|| format!("Api {api_id} no longer exists in state"))?;
15898
15899        if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
15900            api.name = s.to_string();
15901        }
15902        if let Some(s) = props.get("ProtocolType").and_then(|v| v.as_str()) {
15903            api.protocol_type = s.to_string();
15904        }
15905        api.description = props
15906            .get("Description")
15907            .and_then(|v| v.as_str())
15908            .map(String::from)
15909            .or_else(|| api.description.clone());
15910        if let Some(s) = props
15911            .get("RouteSelectionExpression")
15912            .and_then(|v| v.as_str())
15913        {
15914            api.route_selection_expression = s.to_string();
15915        }
15916        if let Some(s) = props
15917            .get("ApiKeySelectionExpression")
15918            .and_then(|v| v.as_str())
15919        {
15920            api.api_key_selection_expression = s.to_string();
15921        }
15922        if let Some(b) = props
15923            .get("DisableExecuteApiEndpoint")
15924            .and_then(|v| v.as_bool())
15925        {
15926            api.disable_execute_api_endpoint = b;
15927        }
15928        if let Some(s) = props.get("IpAddressType").and_then(|v| v.as_str()) {
15929            api.ip_address_type = s.to_string();
15930        }
15931        if let Some(cors) = props.get("CorsConfiguration").and_then(|v| v.as_object()) {
15932            api.cors_configuration = Some(ApiGwV2CorsConfiguration {
15933                allow_credentials: cors.get("AllowCredentials").and_then(|v| v.as_bool()),
15934                allow_headers: cors
15935                    .get("AllowHeaders")
15936                    .and_then(|v| v.as_array())
15937                    .map(|a| {
15938                        a.iter()
15939                            .filter_map(|v| v.as_str().map(String::from))
15940                            .collect()
15941                    }),
15942                allow_methods: cors
15943                    .get("AllowMethods")
15944                    .and_then(|v| v.as_array())
15945                    .map(|a| {
15946                        a.iter()
15947                            .filter_map(|v| v.as_str().map(String::from))
15948                            .collect()
15949                    }),
15950                allow_origins: cors
15951                    .get("AllowOrigins")
15952                    .and_then(|v| v.as_array())
15953                    .map(|a| {
15954                        a.iter()
15955                            .filter_map(|v| v.as_str().map(String::from))
15956                            .collect()
15957                    }),
15958                expose_headers: cors
15959                    .get("ExposeHeaders")
15960                    .and_then(|v| v.as_array())
15961                    .map(|a| {
15962                        a.iter()
15963                            .filter_map(|v| v.as_str().map(String::from))
15964                            .collect()
15965                    }),
15966                max_age: cors
15967                    .get("MaxAge")
15968                    .and_then(|v| v.as_i64())
15969                    .map(|n| n as i32),
15970            });
15971        }
15972        if let Some(obj) = props.get("Tags").and_then(|v| v.as_object()) {
15973            let tags: BTreeMap<String, String> = obj
15974                .iter()
15975                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
15976                .collect();
15977            api.tags = Some(tags);
15978        }
15979
15980        let api_endpoint = api.api_endpoint.clone();
15981        Ok(ProvisionResult::new(api_id.clone())
15982            .with("ApiId", api_id)
15983            .with("ApiEndpoint", api_endpoint))
15984    }
15985
15986    /// In-place update for AWS::ApiGatewayV2::Route. The RouteId is
15987    /// stable across updates; only Target / AuthorizationType /
15988    /// AuthorizerId / RouteKey are rewritten.
15989    fn update_apigwv2_route(
15990        &self,
15991        existing: &StackResource,
15992        resource: &ResourceDefinition,
15993    ) -> Result<ProvisionResult, String> {
15994        let props = &resource.properties;
15995        let api_id = props
15996            .get("ApiId")
15997            .and_then(|v| v.as_str())
15998            .ok_or("ApiId is required")?
15999            .to_string();
16000        let route_id = existing.physical_id.clone();
16001
16002        let mut accounts = self.apigatewayv2_state.write();
16003        let state = accounts.get_or_create(&self.account_id);
16004        let routes = state
16005            .routes
16006            .get_mut(&api_id)
16007            .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16008        let route = routes
16009            .get_mut(&route_id)
16010            .ok_or_else(|| format!("Route {route_id} not yet provisioned for api {api_id}"))?;
16011        if let Some(s) = props.get("RouteKey").and_then(|v| v.as_str()) {
16012            route.route_key = s.to_string();
16013        }
16014        if let Some(s) = props.get("Target").and_then(|v| v.as_str()) {
16015            route.target = Some(s.to_string());
16016        }
16017        if let Some(s) = props.get("AuthorizationType").and_then(|v| v.as_str()) {
16018            route.authorization_type = Some(s.to_string());
16019        }
16020        if let Some(s) = props.get("AuthorizerId").and_then(|v| v.as_str()) {
16021            route.authorizer_id = Some(s.to_string());
16022        }
16023        Ok(ProvisionResult::new(route_id.clone())
16024            .with("RouteId", route_id)
16025            .with("ApiId", api_id))
16026    }
16027
16028    /// In-place update for AWS::ApiGatewayV2::Integration.
16029    fn update_apigwv2_integration(
16030        &self,
16031        existing: &StackResource,
16032        resource: &ResourceDefinition,
16033    ) -> Result<ProvisionResult, String> {
16034        let props = &resource.properties;
16035        let api_id = props
16036            .get("ApiId")
16037            .and_then(|v| v.as_str())
16038            .ok_or("ApiId is required")?
16039            .to_string();
16040        let integration_id = existing.physical_id.clone();
16041
16042        let mut accounts = self.apigatewayv2_state.write();
16043        let state = accounts.get_or_create(&self.account_id);
16044        let integrations = state
16045            .integrations
16046            .get_mut(&api_id)
16047            .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16048        let integ = integrations.get_mut(&integration_id).ok_or_else(|| {
16049            format!("Integration {integration_id} not yet provisioned for api {api_id}")
16050        })?;
16051        if let Some(s) = props.get("IntegrationType").and_then(|v| v.as_str()) {
16052            integ.integration_type = s.to_string();
16053        }
16054        if let Some(s) = props.get("IntegrationUri").and_then(|v| v.as_str()) {
16055            integ.integration_uri = Some(s.to_string());
16056        }
16057        if let Some(s) = props.get("PayloadFormatVersion").and_then(|v| v.as_str()) {
16058            integ.payload_format_version = Some(s.to_string());
16059        }
16060        if let Some(n) = props.get("TimeoutInMillis").and_then(|v| v.as_i64()) {
16061            integ.timeout_in_millis = Some(n);
16062        }
16063        Ok(ProvisionResult::new(integration_id.clone())
16064            .with("IntegrationId", integration_id)
16065            .with("ApiId", api_id))
16066    }
16067
16068    /// In-place update for AWS::ApiGatewayV2::IntegrationResponse. The
16069    /// composite physical id `{integration_id}/{response_id}` is stable
16070    /// across updates; only the embedded response body is rewritten.
16071    fn update_apigwv2_integration_response(
16072        &self,
16073        existing: &StackResource,
16074        resource: &ResourceDefinition,
16075    ) -> Result<ProvisionResult, String> {
16076        let props = &resource.properties;
16077        let api_id = props
16078            .get("ApiId")
16079            .and_then(|v| v.as_str())
16080            .ok_or("ApiId is required")?
16081            .to_string();
16082        let composite_key = existing.physical_id.clone();
16083        let (integration_id, response_id) = composite_key
16084            .split_once('/')
16085            .map(|(a, b)| (a.to_string(), b.to_string()))
16086            .ok_or_else(|| format!("Invalid IntegrationResponse physical id: {composite_key}"))?;
16087        let key_expr = props
16088            .get("IntegrationResponseKey")
16089            .and_then(|v| v.as_str())
16090            .ok_or("IntegrationResponseKey is required")?
16091            .to_string();
16092        let body = serde_json::json!({
16093            "integrationResponseId": response_id,
16094            "integrationId": integration_id,
16095            "integrationResponseKey": key_expr,
16096            "responseTemplates": props.get("ResponseTemplates").cloned().unwrap_or(serde_json::json!({})),
16097            "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
16098            "templateSelectionExpression": props.get("TemplateSelectionExpression").and_then(|v| v.as_str()),
16099            "contentHandlingStrategy": props.get("ContentHandlingStrategy").and_then(|v| v.as_str()),
16100        });
16101        let mut accounts = self.apigatewayv2_state.write();
16102        let state = accounts.get_or_create(&self.account_id);
16103        let map = state
16104            .integration_responses
16105            .get_mut(&api_id)
16106            .ok_or_else(|| format!("No integration responses found for api {api_id}"))?;
16107        if !map.contains_key(&composite_key) {
16108            return Err(format!(
16109                "IntegrationResponse {composite_key} not yet provisioned for api {api_id}"
16110            ));
16111        }
16112        map.insert(composite_key.clone(), body);
16113        Ok(ProvisionResult::new(composite_key)
16114            .with("IntegrationResponseId", response_id)
16115            .with("IntegrationId", integration_id)
16116            .with("ApiId", api_id))
16117    }
16118
16119    /// In-place update for AWS::ApiGatewayV2::RouteResponse. Composite
16120    /// physical id `{route_id}/{response_id}` is preserved.
16121    fn update_apigwv2_route_response(
16122        &self,
16123        existing: &StackResource,
16124        resource: &ResourceDefinition,
16125    ) -> Result<ProvisionResult, String> {
16126        let props = &resource.properties;
16127        let api_id = props
16128            .get("ApiId")
16129            .and_then(|v| v.as_str())
16130            .ok_or("ApiId is required")?
16131            .to_string();
16132        let composite = existing.physical_id.clone();
16133        let (route_id, response_id) = composite
16134            .split_once('/')
16135            .map(|(a, b)| (a.to_string(), b.to_string()))
16136            .ok_or_else(|| format!("Invalid RouteResponse physical id: {composite}"))?;
16137        let key_expr = props
16138            .get("RouteResponseKey")
16139            .and_then(|v| v.as_str())
16140            .ok_or("RouteResponseKey is required")?
16141            .to_string();
16142        let body = serde_json::json!({
16143            "routeResponseId": response_id,
16144            "routeId": route_id,
16145            "routeResponseKey": key_expr,
16146            "responseModels": props.get("ResponseModels").cloned().unwrap_or(serde_json::json!({})),
16147            "modelSelectionExpression": props.get("ModelSelectionExpression").and_then(|v| v.as_str()),
16148            "responseParameters": props.get("ResponseParameters").cloned().unwrap_or(serde_json::json!({})),
16149        });
16150        let mut accounts = self.apigatewayv2_state.write();
16151        let state = accounts.get_or_create(&self.account_id);
16152        let map = state
16153            .route_responses
16154            .get_mut(&api_id)
16155            .ok_or_else(|| format!("No route responses found for api {api_id}"))?;
16156        if !map.contains_key(&composite) {
16157            return Err(format!(
16158                "RouteResponse {composite} not yet provisioned for api {api_id}"
16159            ));
16160        }
16161        map.insert(composite.clone(), body);
16162        Ok(ProvisionResult::new(composite)
16163            .with("RouteResponseId", response_id)
16164            .with("RouteId", route_id)
16165            .with("ApiId", api_id))
16166    }
16167
16168    /// In-place update for AWS::ApiGatewayV2::Stage. StageName is the
16169    /// physical id and stays stable; description / deployment id /
16170    /// auto-deploy can change.
16171    fn update_apigwv2_stage(
16172        &self,
16173        existing: &StackResource,
16174        resource: &ResourceDefinition,
16175    ) -> Result<ProvisionResult, String> {
16176        let props = &resource.properties;
16177        let api_id = props
16178            .get("ApiId")
16179            .and_then(|v| v.as_str())
16180            .ok_or("ApiId is required")?
16181            .to_string();
16182        let stage_name = existing.physical_id.clone();
16183
16184        let mut accounts = self.apigatewayv2_state.write();
16185        let state = accounts.get_or_create(&self.account_id);
16186        let stages = state
16187            .stages
16188            .get_mut(&api_id)
16189            .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16190        let stage = stages
16191            .get_mut(&stage_name)
16192            .ok_or_else(|| format!("Stage {stage_name} not yet provisioned for api {api_id}"))?;
16193        if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
16194            stage.description = Some(s.to_string());
16195        }
16196        if let Some(s) = props.get("DeploymentId").and_then(|v| v.as_str()) {
16197            stage.deployment_id = Some(s.to_string());
16198        }
16199        if let Some(b) = props.get("AutoDeploy").and_then(|v| v.as_bool()) {
16200            stage.auto_deploy = b;
16201        }
16202        stage.last_updated_date = Some(Utc::now());
16203        Ok(ProvisionResult::new(stage_name.clone())
16204            .with("StageName", stage_name)
16205            .with("ApiId", api_id))
16206    }
16207
16208    /// In-place update for AWS::ApiGatewayV2::Deployment. CFN allows
16209    /// editing the Description; the DeploymentId is stable.
16210    fn update_apigwv2_deployment(
16211        &self,
16212        existing: &StackResource,
16213        resource: &ResourceDefinition,
16214    ) -> Result<ProvisionResult, String> {
16215        let props = &resource.properties;
16216        let api_id = props
16217            .get("ApiId")
16218            .and_then(|v| v.as_str())
16219            .ok_or("ApiId is required")?
16220            .to_string();
16221        let deployment_id = existing.physical_id.clone();
16222        let mut accounts = self.apigatewayv2_state.write();
16223        let state = accounts.get_or_create(&self.account_id);
16224        let deps = state
16225            .deployments
16226            .get_mut(&api_id)
16227            .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16228        let dep = deps.get_mut(&deployment_id).ok_or_else(|| {
16229            format!("Deployment {deployment_id} not yet provisioned for api {api_id}")
16230        })?;
16231        if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
16232            dep.description = Some(s.to_string());
16233        }
16234        Ok(ProvisionResult::new(deployment_id.clone())
16235            .with("DeploymentId", deployment_id)
16236            .with("ApiId", api_id))
16237    }
16238
16239    /// In-place update for AWS::ApiGatewayV2::Authorizer. Mutates the
16240    /// stored authorizer in place; the AuthorizerId remains stable.
16241    fn update_apigwv2_authorizer(
16242        &self,
16243        existing: &StackResource,
16244        resource: &ResourceDefinition,
16245    ) -> Result<ProvisionResult, String> {
16246        let props = &resource.properties;
16247        let api_id = props
16248            .get("ApiId")
16249            .and_then(|v| v.as_str())
16250            .ok_or("ApiId is required")?
16251            .to_string();
16252        let authorizer_id = existing.physical_id.clone();
16253
16254        let mut accounts = self.apigatewayv2_state.write();
16255        let state = accounts.get_or_create(&self.account_id);
16256        let auths = state
16257            .authorizers
16258            .get_mut(&api_id)
16259            .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16260        let auth = auths.get_mut(&authorizer_id).ok_or_else(|| {
16261            format!("Authorizer {authorizer_id} not yet provisioned for api {api_id}")
16262        })?;
16263        if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
16264            auth.name = s.to_string();
16265        }
16266        if let Some(s) = props.get("AuthorizerType").and_then(|v| v.as_str()) {
16267            auth.authorizer_type = s.to_string();
16268        }
16269        if let Some(s) = props.get("AuthorizerUri").and_then(|v| v.as_str()) {
16270            auth.authorizer_uri = Some(s.to_string());
16271        }
16272        if let Some(arr) = props.get("IdentitySource").and_then(|v| v.as_array()) {
16273            auth.identity_source = Some(
16274                arr.iter()
16275                    .filter_map(|v| v.as_str().map(String::from))
16276                    .collect(),
16277            );
16278        }
16279        if let Some(obj) = props.get("JwtConfiguration").and_then(|v| v.as_object()) {
16280            auth.jwt_configuration = Some(ApiGwV2JwtConfiguration {
16281                audience: obj.get("Audience").and_then(|v| v.as_array()).map(|a| {
16282                    a.iter()
16283                        .filter_map(|v| v.as_str().map(String::from))
16284                        .collect()
16285                }),
16286                issuer: obj.get("Issuer").and_then(|v| v.as_str()).map(String::from),
16287            });
16288        }
16289        Ok(ProvisionResult::new(authorizer_id.clone())
16290            .with("AuthorizerId", authorizer_id)
16291            .with("ApiId", api_id))
16292    }
16293
16294    /// In-place update for AWS::ApiGatewayV2::DomainName. The DomainName
16295    /// is the physical id and stays stable; configuration / mTLS can
16296    /// change.
16297    fn update_apigwv2_domain_name(
16298        &self,
16299        existing: &StackResource,
16300        resource: &ResourceDefinition,
16301    ) -> Result<ProvisionResult, String> {
16302        let props = &resource.properties;
16303        let domain_name = existing.physical_id.clone();
16304        let body = serde_json::json!({
16305            "domainName": domain_name,
16306            "domainNameConfigurations": props.get("DomainNameConfigurations").cloned().unwrap_or(serde_json::json!([])),
16307            "mutualTlsAuthentication": props.get("MutualTlsAuthentication").cloned(),
16308            "apiMappingSelectionExpression": null,
16309        });
16310        let mut accounts = self.apigatewayv2_state.write();
16311        let state = accounts.get_or_create(&self.account_id);
16312        if !state.domain_names.contains_key(&domain_name) {
16313            return Err(format!("DomainName {domain_name} no longer exists"));
16314        }
16315        state.domain_names.insert(domain_name.clone(), body);
16316        Ok(ProvisionResult::new(domain_name.clone()).with("DomainName", domain_name))
16317    }
16318
16319    /// In-place update for AWS::ApiGatewayV2::ApiMapping. The ApiMappingId
16320    /// is the physical id; ApiId / Stage / ApiMappingKey can change.
16321    fn update_apigwv2_api_mapping(
16322        &self,
16323        existing: &StackResource,
16324        resource: &ResourceDefinition,
16325    ) -> Result<ProvisionResult, String> {
16326        let props = &resource.properties;
16327        let domain_name = props
16328            .get("DomainName")
16329            .and_then(|v| v.as_str())
16330            .ok_or("DomainName is required")?
16331            .to_string();
16332        let api_id = props
16333            .get("ApiId")
16334            .and_then(|v| v.as_str())
16335            .ok_or("ApiId is required")?
16336            .to_string();
16337        let stage = props
16338            .get("Stage")
16339            .and_then(|v| v.as_str())
16340            .ok_or("Stage is required")?
16341            .to_string();
16342        let api_mapping_key = props
16343            .get("ApiMappingKey")
16344            .and_then(|v| v.as_str())
16345            .map(String::from);
16346        let id = existing.physical_id.clone();
16347        let body = serde_json::json!({
16348            "apiMappingId": id,
16349            "apiId": api_id,
16350            "stage": stage,
16351            "apiMappingKey": api_mapping_key,
16352        });
16353        let mut accounts = self.apigatewayv2_state.write();
16354        let state = accounts.get_or_create(&self.account_id);
16355        let map = state
16356            .api_mappings
16357            .get_mut(&domain_name)
16358            .ok_or_else(|| format!("DomainName {domain_name} no longer exists"))?;
16359        map.insert(id.clone(), body);
16360        Ok(ProvisionResult::new(id.clone())
16361            .with("ApiMappingId", id)
16362            .with("DomainName", domain_name))
16363    }
16364
16365    /// In-place update for AWS::ApiGatewayV2::VpcLink. The VpcLinkId is
16366    /// the physical id; Name / SubnetIds / SecurityGroupIds / Tags can
16367    /// change.
16368    fn update_apigwv2_vpc_link(
16369        &self,
16370        existing: &StackResource,
16371        resource: &ResourceDefinition,
16372    ) -> Result<ProvisionResult, String> {
16373        let props = &resource.properties;
16374        let id = existing.physical_id.clone();
16375        let mut accounts = self.apigatewayv2_state.write();
16376        let state = accounts.get_or_create(&self.account_id);
16377        let body = state
16378            .vpc_links
16379            .get_mut(&id)
16380            .ok_or_else(|| format!("VpcLink {id} no longer exists"))?;
16381        if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
16382            body["name"] = serde_json::Value::String(s.to_string());
16383        }
16384        if let Some(v) = props.get("SubnetIds").cloned() {
16385            body["subnetIds"] = v;
16386        }
16387        if let Some(v) = props.get("SecurityGroupIds").cloned() {
16388            body["securityGroupIds"] = v;
16389        }
16390        if let Some(v) = props.get("Tags").cloned() {
16391            body["tags"] = v;
16392        }
16393        Ok(ProvisionResult::new(id.clone()).with("VpcLinkId", id))
16394    }
16395
16396    /// In-place update for AWS::ApiGatewayV2::Model. The ModelId is the
16397    /// physical id and stays stable; Schema / Description / ContentType
16398    /// / Name can change.
16399    fn update_apigwv2_model(
16400        &self,
16401        existing: &StackResource,
16402        resource: &ResourceDefinition,
16403    ) -> Result<ProvisionResult, String> {
16404        let props = &resource.properties;
16405        let api_id = props
16406            .get("ApiId")
16407            .and_then(|v| v.as_str())
16408            .ok_or("ApiId is required")?
16409            .to_string();
16410        let id = existing.physical_id.clone();
16411        let mut accounts = self.apigatewayv2_state.write();
16412        let state = accounts.get_or_create(&self.account_id);
16413        let map = state
16414            .models
16415            .get_mut(&api_id)
16416            .ok_or_else(|| format!("Api {api_id} not yet provisioned"))?;
16417        let body = map
16418            .get_mut(&id)
16419            .ok_or_else(|| format!("Model {id} not yet provisioned for api {api_id}"))?;
16420        if let Some(s) = props.get("Name").and_then(|v| v.as_str()) {
16421            body["name"] = serde_json::Value::String(s.to_string());
16422        }
16423        if let Some(s) = props.get("ContentType").and_then(|v| v.as_str()) {
16424            body["contentType"] = serde_json::Value::String(s.to_string());
16425        }
16426        if let Some(s) = props.get("Description").and_then(|v| v.as_str()) {
16427            body["description"] = serde_json::Value::String(s.to_string());
16428        }
16429        if let Some(v) = props.get("Schema") {
16430            body["schema"] = serde_json::Value::String(if let Some(s) = v.as_str() {
16431                s.to_string()
16432            } else {
16433                v.to_string()
16434            });
16435        }
16436        Ok(ProvisionResult::new(id.clone())
16437            .with("ModelId", id)
16438            .with("ApiId", api_id))
16439    }
16440
16441    // --- SES ---
16442
16443    fn create_ses_configuration_set(
16444        &self,
16445        resource: &ResourceDefinition,
16446    ) -> Result<ProvisionResult, String> {
16447        let props = &resource.properties;
16448        let name = props
16449            .get("Name")
16450            .and_then(|v| v.as_str())
16451            .map(String::from)
16452            .unwrap_or_else(|| format!("cfn-cs-{}", resource.logical_id));
16453        let sending_enabled = props
16454            .get("SendingOptions")
16455            .and_then(|v| v.get("SendingEnabled"))
16456            .and_then(|v| v.as_bool())
16457            .unwrap_or(true);
16458        let tls_policy = props
16459            .get("DeliveryOptions")
16460            .and_then(|v| v.get("TlsPolicy"))
16461            .and_then(|v| v.as_str())
16462            .unwrap_or("OPTIONAL")
16463            .to_string();
16464        let sending_pool_name = props
16465            .get("DeliveryOptions")
16466            .and_then(|v| v.get("SendingPoolName"))
16467            .and_then(|v| v.as_str())
16468            .map(String::from);
16469        let custom_redirect_domain = props
16470            .get("TrackingOptions")
16471            .and_then(|v| v.get("CustomRedirectDomain"))
16472            .and_then(|v| v.as_str())
16473            .map(String::from);
16474        let suppressed_reasons: Vec<String> = props
16475            .get("SuppressionOptions")
16476            .and_then(|v| v.get("SuppressedReasons"))
16477            .and_then(|v| v.as_array())
16478            .map(|arr| {
16479                arr.iter()
16480                    .filter_map(|v| v.as_str().map(String::from))
16481                    .collect()
16482            })
16483            .unwrap_or_default();
16484        let reputation_metrics_enabled = props
16485            .get("ReputationOptions")
16486            .and_then(|v| v.get("ReputationMetricsEnabled"))
16487            .and_then(|v| v.as_bool())
16488            .unwrap_or(false);
16489
16490        let cs = SesConfigurationSet {
16491            name: name.clone(),
16492            sending_enabled,
16493            tls_policy,
16494            sending_pool_name,
16495            custom_redirect_domain,
16496            https_policy: props
16497                .get("TrackingOptions")
16498                .and_then(|v| v.get("HttpsPolicy"))
16499                .and_then(|v| v.as_str())
16500                .map(String::from),
16501            suppressed_reasons,
16502            reputation_metrics_enabled,
16503            vdm_options: props.get("VdmOptions").cloned(),
16504            archive_arn: props
16505                .get("ArchivingOptions")
16506                .and_then(|v| v.get("ArchiveArn"))
16507                .and_then(|v| v.as_str())
16508                .map(String::from),
16509            archiving_options_present: props.get("ArchivingOptions").is_some_and(|v| v.is_object()),
16510        };
16511        let mut accounts = self.ses_state.write();
16512        let state = accounts.get_or_create(&self.account_id);
16513        state.configuration_sets.insert(name.clone(), cs);
16514        Ok(ProvisionResult::new(name.clone()).with("Name", name))
16515    }
16516
16517    fn delete_ses_configuration_set(&self, physical_id: &str) -> Result<(), String> {
16518        let mut accounts = self.ses_state.write();
16519        let state = accounts.get_or_create(&self.account_id);
16520        state.configuration_sets.remove(physical_id);
16521        state.event_destinations.remove(physical_id);
16522        Ok(())
16523    }
16524
16525    fn create_ses_event_destination(
16526        &self,
16527        resource: &ResourceDefinition,
16528    ) -> Result<ProvisionResult, String> {
16529        let props = &resource.properties;
16530        let cs_name = props
16531            .get("ConfigurationSetName")
16532            .and_then(|v| v.as_str())
16533            .ok_or("ConfigurationSetName is required")?
16534            .to_string();
16535        let dest_props = props
16536            .get("EventDestination")
16537            .and_then(|v| v.as_object())
16538            .ok_or("EventDestination is required")?;
16539        let name = dest_props
16540            .get("Name")
16541            .and_then(|v| v.as_str())
16542            .map(String::from)
16543            .unwrap_or_else(|| format!("cfn-ed-{}", resource.logical_id));
16544        let enabled = dest_props
16545            .get("Enabled")
16546            .and_then(|v| v.as_bool())
16547            .unwrap_or(true);
16548        let matching_event_types: Vec<String> = dest_props
16549            .get("MatchingEventTypes")
16550            .and_then(|v| v.as_array())
16551            .map(|arr| {
16552                arr.iter()
16553                    .filter_map(|v| v.as_str().map(String::from))
16554                    .collect()
16555            })
16556            .unwrap_or_default();
16557        let dest = SesEventDestination {
16558            name: name.clone(),
16559            enabled,
16560            matching_event_types,
16561            kinesis_firehose_destination: dest_props.get("KinesisFirehoseDestination").cloned(),
16562            cloud_watch_destination: dest_props.get("CloudWatchDestination").cloned(),
16563            sns_destination: dest_props.get("SnsDestination").cloned(),
16564            event_bridge_destination: dest_props.get("EventBridgeDestination").cloned(),
16565            pinpoint_destination: dest_props.get("PinpointDestination").cloned(),
16566        };
16567        let mut accounts = self.ses_state.write();
16568        let state = accounts.get_or_create(&self.account_id);
16569        if !state.configuration_sets.contains_key(&cs_name) {
16570            return Err(format!("ConfigurationSet {cs_name} not yet provisioned"));
16571        }
16572        let dests = state.event_destinations.entry(cs_name.clone()).or_default();
16573        dests.retain(|d| d.name != name);
16574        dests.push(dest);
16575        let physical = format!("{cs_name}|{name}");
16576        Ok(ProvisionResult::new(physical)
16577            .with("Name", name)
16578            .with("ConfigurationSetName", cs_name))
16579    }
16580
16581    fn delete_ses_event_destination(
16582        &self,
16583        physical_id: &str,
16584        _attributes: &BTreeMap<String, String>,
16585    ) -> Result<(), String> {
16586        let mut parts = physical_id.splitn(2, '|');
16587        let Some(cs) = parts.next() else {
16588            return Ok(());
16589        };
16590        let Some(name) = parts.next() else {
16591            return Ok(());
16592        };
16593        let mut accounts = self.ses_state.write();
16594        let state = accounts.get_or_create(&self.account_id);
16595        if let Some(dests) = state.event_destinations.get_mut(cs) {
16596            dests.retain(|d| d.name != name);
16597        }
16598        Ok(())
16599    }
16600
16601    fn create_ses_email_identity(
16602        &self,
16603        resource: &ResourceDefinition,
16604    ) -> Result<ProvisionResult, String> {
16605        let props = &resource.properties;
16606        let identity_name = props
16607            .get("EmailIdentity")
16608            .and_then(|v| v.as_str())
16609            .ok_or("EmailIdentity is required")?
16610            .to_string();
16611        let identity_type = if identity_name.contains('@') {
16612            "EMAIL_ADDRESS"
16613        } else {
16614            "DOMAIN"
16615        }
16616        .to_string();
16617        let dkim_signing_enabled = props
16618            .get("DkimAttributes")
16619            .and_then(|v| v.get("SigningEnabled"))
16620            .and_then(|v| v.as_bool())
16621            .unwrap_or(true);
16622        let dkim_signing_attributes_origin = props
16623            .get("DkimSigningAttributes")
16624            .map(|_| "EXTERNAL")
16625            .unwrap_or("AWS_SES")
16626            .to_string();
16627        let mail_from_domain = props
16628            .get("MailFromAttributes")
16629            .and_then(|v| v.get("MailFromDomain"))
16630            .and_then(|v| v.as_str())
16631            .map(String::from);
16632        let mail_from_behavior = props
16633            .get("MailFromAttributes")
16634            .and_then(|v| v.get("BehaviorOnMxFailure"))
16635            .and_then(|v| v.as_str())
16636            .unwrap_or("USE_DEFAULT_VALUE")
16637            .to_string();
16638        let configuration_set_name = props
16639            .get("ConfigurationSetAttributes")
16640            .and_then(|v| v.get("ConfigurationSetName"))
16641            .and_then(|v| v.as_str())
16642            .map(String::from);
16643        let email_forwarding_enabled = props
16644            .get("FeedbackAttributes")
16645            .and_then(|v| v.get("EmailForwardingEnabled"))
16646            .and_then(|v| v.as_bool())
16647            .unwrap_or(true);
16648
16649        let identity = SesEmailIdentity {
16650            identity_name: identity_name.clone(),
16651            identity_type,
16652            verified: true,
16653            created_at: Utc::now(),
16654            dkim_signing_enabled,
16655            dkim_signing_attributes_origin,
16656            dkim_domain_signing_private_key: None,
16657            dkim_domain_signing_selector: None,
16658            dkim_next_signing_key_length: None,
16659            dkim_public_key_b64: None,
16660            email_forwarding_enabled,
16661            mail_from_domain,
16662            mail_from_behavior_on_mx_failure: mail_from_behavior,
16663            mail_from_domain_status: "Success".to_string(),
16664            configuration_set_name,
16665        };
16666        let mut accounts = self.ses_state.write();
16667        let state = accounts.get_or_create(&self.account_id);
16668        state.identities.insert(identity_name.clone(), identity);
16669        Ok(ProvisionResult::new(identity_name.clone()).with("EmailIdentity", identity_name))
16670    }
16671
16672    fn delete_ses_email_identity(&self, physical_id: &str) -> Result<(), String> {
16673        let mut accounts = self.ses_state.write();
16674        let state = accounts.get_or_create(&self.account_id);
16675        state.identities.remove(physical_id);
16676        Ok(())
16677    }
16678
16679    fn create_ses_template(
16680        &self,
16681        resource: &ResourceDefinition,
16682    ) -> Result<ProvisionResult, String> {
16683        let props = &resource.properties;
16684        let template_block = props
16685            .get("Template")
16686            .and_then(|v| v.as_object())
16687            .ok_or("Template is required")?;
16688        let template_name = template_block
16689            .get("TemplateName")
16690            .and_then(|v| v.as_str())
16691            .map(String::from)
16692            .unwrap_or_else(|| format!("cfn-tpl-{}", resource.logical_id));
16693        let tpl = SesEmailTemplate {
16694            template_name: template_name.clone(),
16695            subject: template_block
16696                .get("SubjectPart")
16697                .and_then(|v| v.as_str())
16698                .map(String::from),
16699            html_body: template_block
16700                .get("HtmlPart")
16701                .and_then(|v| v.as_str())
16702                .map(String::from),
16703            text_body: template_block
16704                .get("TextPart")
16705                .and_then(|v| v.as_str())
16706                .map(String::from),
16707            created_at: Utc::now(),
16708        };
16709        let mut accounts = self.ses_state.write();
16710        let state = accounts.get_or_create(&self.account_id);
16711        state.templates.insert(template_name.clone(), tpl);
16712        Ok(ProvisionResult::new(template_name.clone()).with("TemplateName", template_name))
16713    }
16714
16715    fn delete_ses_template(&self, physical_id: &str) -> Result<(), String> {
16716        let mut accounts = self.ses_state.write();
16717        let state = accounts.get_or_create(&self.account_id);
16718        state.templates.remove(physical_id);
16719        Ok(())
16720    }
16721
16722    fn create_ses_contact_list(
16723        &self,
16724        resource: &ResourceDefinition,
16725    ) -> Result<ProvisionResult, String> {
16726        let props = &resource.properties;
16727        let name = props
16728            .get("ContactListName")
16729            .and_then(|v| v.as_str())
16730            .map(String::from)
16731            .unwrap_or_else(|| format!("cfn-cl-{}", resource.logical_id));
16732        let description = props
16733            .get("Description")
16734            .and_then(|v| v.as_str())
16735            .map(String::from);
16736        let now = Utc::now();
16737        let cl = SesContactList {
16738            contact_list_name: name.clone(),
16739            description,
16740            topics: Vec::new(),
16741            created_at: now,
16742            last_updated_at: now,
16743        };
16744        let mut accounts = self.ses_state.write();
16745        let state = accounts.get_or_create(&self.account_id);
16746        state.contact_lists.insert(name.clone(), cl);
16747        Ok(ProvisionResult::new(name.clone()).with("ContactListName", name))
16748    }
16749
16750    fn delete_ses_contact_list(&self, physical_id: &str) -> Result<(), String> {
16751        let mut accounts = self.ses_state.write();
16752        let state = accounts.get_or_create(&self.account_id);
16753        state.contact_lists.remove(physical_id);
16754        state.contacts.remove(physical_id);
16755        Ok(())
16756    }
16757
16758    fn create_ses_dedicated_ip_pool(
16759        &self,
16760        resource: &ResourceDefinition,
16761    ) -> Result<ProvisionResult, String> {
16762        let props = &resource.properties;
16763        let name = props
16764            .get("PoolName")
16765            .and_then(|v| v.as_str())
16766            .map(String::from)
16767            .unwrap_or_else(|| format!("cfn-pool-{}", resource.logical_id));
16768        let scaling_mode = props
16769            .get("ScalingMode")
16770            .and_then(|v| v.as_str())
16771            .unwrap_or("STANDARD")
16772            .to_string();
16773        let pool = SesDedicatedIpPool {
16774            pool_name: name.clone(),
16775            scaling_mode,
16776        };
16777        let mut accounts = self.ses_state.write();
16778        let state = accounts.get_or_create(&self.account_id);
16779        state.dedicated_ip_pools.insert(name.clone(), pool);
16780        Ok(ProvisionResult::new(name.clone()).with("PoolName", name))
16781    }
16782
16783    fn delete_ses_dedicated_ip_pool(&self, physical_id: &str) -> Result<(), String> {
16784        let mut accounts = self.ses_state.write();
16785        let state = accounts.get_or_create(&self.account_id);
16786        state.dedicated_ip_pools.remove(physical_id);
16787        Ok(())
16788    }
16789
16790    fn create_ses_receipt_rule_set(
16791        &self,
16792        resource: &ResourceDefinition,
16793    ) -> Result<ProvisionResult, String> {
16794        let props = &resource.properties;
16795        let name = props
16796            .get("RuleSetName")
16797            .and_then(|v| v.as_str())
16798            .map(String::from)
16799            .unwrap_or_else(|| format!("cfn-rs-{}", resource.logical_id));
16800        let rs = SesReceiptRuleSet {
16801            name: name.clone(),
16802            rules: Vec::new(),
16803            created_at: Utc::now(),
16804        };
16805        let mut accounts = self.ses_state.write();
16806        let state = accounts.get_or_create(&self.account_id);
16807        state.receipt_rule_sets.insert(name.clone(), rs);
16808        Ok(ProvisionResult::new(name.clone()).with("RuleSetName", name))
16809    }
16810
16811    fn delete_ses_receipt_rule_set(&self, physical_id: &str) -> Result<(), String> {
16812        let mut accounts = self.ses_state.write();
16813        let state = accounts.get_or_create(&self.account_id);
16814        state.receipt_rule_sets.remove(physical_id);
16815        Ok(())
16816    }
16817
16818    fn create_ses_receipt_rule(
16819        &self,
16820        resource: &ResourceDefinition,
16821    ) -> Result<ProvisionResult, String> {
16822        let props = &resource.properties;
16823        let rule_set_name = props
16824            .get("RuleSetName")
16825            .and_then(|v| v.as_str())
16826            .ok_or("RuleSetName is required")?
16827            .to_string();
16828        let rule_block = props
16829            .get("Rule")
16830            .and_then(|v| v.as_object())
16831            .ok_or("Rule is required")?;
16832        let name = rule_block
16833            .get("Name")
16834            .and_then(|v| v.as_str())
16835            .map(String::from)
16836            .unwrap_or_else(|| format!("cfn-rule-{}", resource.logical_id));
16837        let enabled = rule_block
16838            .get("Enabled")
16839            .and_then(|v| v.as_bool())
16840            .unwrap_or(true);
16841        let scan_enabled = rule_block
16842            .get("ScanEnabled")
16843            .and_then(|v| v.as_bool())
16844            .unwrap_or(false);
16845        let tls_policy = rule_block
16846            .get("TlsPolicy")
16847            .and_then(|v| v.as_str())
16848            .unwrap_or("Optional")
16849            .to_string();
16850        let recipients: Vec<String> = rule_block
16851            .get("Recipients")
16852            .and_then(|v| v.as_array())
16853            .map(|arr| {
16854                arr.iter()
16855                    .filter_map(|v| v.as_str().map(String::from))
16856                    .collect()
16857            })
16858            .unwrap_or_default();
16859        let actions: Vec<SesReceiptAction> = rule_block
16860            .get("Actions")
16861            .and_then(|v| v.as_array())
16862            .map(|arr| arr.iter().filter_map(parse_ses_receipt_action).collect())
16863            .unwrap_or_default();
16864
16865        let rule = SesReceiptRule {
16866            name: name.clone(),
16867            enabled,
16868            scan_enabled,
16869            tls_policy,
16870            recipients,
16871            actions,
16872        };
16873        let mut accounts = self.ses_state.write();
16874        let state = accounts.get_or_create(&self.account_id);
16875        let rs = state
16876            .receipt_rule_sets
16877            .get_mut(&rule_set_name)
16878            .ok_or_else(|| format!("ReceiptRuleSet {rule_set_name} not yet provisioned"))?;
16879        rs.rules.retain(|r| r.name != name);
16880        rs.rules.push(rule);
16881        let physical = format!("{rule_set_name}|{name}");
16882        Ok(ProvisionResult::new(physical)
16883            .with("Name", name)
16884            .with("RuleSetName", rule_set_name))
16885    }
16886
16887    fn delete_ses_receipt_rule(
16888        &self,
16889        physical_id: &str,
16890        _attributes: &BTreeMap<String, String>,
16891    ) -> Result<(), String> {
16892        let mut parts = physical_id.splitn(2, '|');
16893        let Some(rs_name) = parts.next() else {
16894            return Ok(());
16895        };
16896        let Some(rule_name) = parts.next() else {
16897            return Ok(());
16898        };
16899        let mut accounts = self.ses_state.write();
16900        let state = accounts.get_or_create(&self.account_id);
16901        if let Some(rs) = state.receipt_rule_sets.get_mut(rs_name) {
16902            rs.rules.retain(|r| r.name != rule_name);
16903        }
16904        Ok(())
16905    }
16906
16907    fn create_ses_receipt_filter(
16908        &self,
16909        resource: &ResourceDefinition,
16910    ) -> Result<ProvisionResult, String> {
16911        let props = &resource.properties;
16912        let filter_block = props
16913            .get("Filter")
16914            .and_then(|v| v.as_object())
16915            .ok_or("Filter is required")?;
16916        let name = filter_block
16917            .get("Name")
16918            .and_then(|v| v.as_str())
16919            .map(String::from)
16920            .unwrap_or_else(|| format!("cfn-filter-{}", resource.logical_id));
16921        let ip_block = filter_block
16922            .get("IpFilter")
16923            .and_then(|v| v.as_object())
16924            .ok_or("Filter.IpFilter is required")?;
16925        let cidr = ip_block
16926            .get("Cidr")
16927            .and_then(|v| v.as_str())
16928            .ok_or("Filter.IpFilter.Cidr is required")?
16929            .to_string();
16930        let policy = ip_block
16931            .get("Policy")
16932            .and_then(|v| v.as_str())
16933            .unwrap_or("Block")
16934            .to_string();
16935        let filter = SesReceiptFilter {
16936            name: name.clone(),
16937            ip_filter: SesIpFilter { cidr, policy },
16938        };
16939        let mut accounts = self.ses_state.write();
16940        let state = accounts.get_or_create(&self.account_id);
16941        state.receipt_filters.insert(name.clone(), filter);
16942        Ok(ProvisionResult::new(name.clone()).with("Name", name))
16943    }
16944
16945    fn delete_ses_receipt_filter(&self, physical_id: &str) -> Result<(), String> {
16946        let mut accounts = self.ses_state.write();
16947        let state = accounts.get_or_create(&self.account_id);
16948        state.receipt_filters.remove(physical_id);
16949        Ok(())
16950    }
16951
16952    fn create_ses_vdm_attributes(
16953        &self,
16954        resource: &ResourceDefinition,
16955    ) -> Result<ProvisionResult, String> {
16956        let props = &resource.properties;
16957        let mut accounts = self.ses_state.write();
16958        let state = accounts.get_or_create(&self.account_id);
16959        state.account_settings.vdm_attributes = Some(props.clone());
16960        Ok(ProvisionResult::new(format!("vdm-{}", resource.logical_id)))
16961    }
16962
16963    // --- SecretsManager extras ---
16964
16965    fn create_secrets_manager_rotation_schedule(
16966        &self,
16967        resource: &ResourceDefinition,
16968    ) -> Result<ProvisionResult, String> {
16969        let props = &resource.properties;
16970        let secret_id = props
16971            .get("SecretId")
16972            .and_then(|v| v.as_str())
16973            .ok_or("SecretId is required")?
16974            .to_string();
16975        let rotation_lambda_arn = props
16976            .get("RotationLambdaARN")
16977            .and_then(|v| v.as_str())
16978            .map(String::from);
16979        let automatically_after_days = props
16980            .get("RotationRules")
16981            .and_then(|v| v.get("AutomaticallyAfterDays"))
16982            .and_then(|v| v.as_i64());
16983        let mut accounts = self.secretsmanager_state.write();
16984        let state = accounts.get_or_create(&self.account_id);
16985        let secret_arn = if state.secrets.contains_key(&secret_id) {
16986            secret_id.clone()
16987        } else {
16988            let candidate = format!(
16989                "arn:aws:secretsmanager:{}:{}:secret:{}",
16990                state.region, state.account_id, secret_id
16991            );
16992            if state.secrets.contains_key(&candidate) {
16993                candidate
16994            } else {
16995                return Err(format!("Secret {secret_id} not yet provisioned"));
16996            }
16997        };
16998        let secret = state
16999            .secrets
17000            .get_mut(&secret_arn)
17001            .ok_or_else(|| format!("Secret {secret_arn} not found"))?;
17002        secret.rotation_enabled = Some(true);
17003        secret.rotation_lambda_arn = rotation_lambda_arn;
17004        secret.rotation_rules = Some(RotationRules {
17005            automatically_after_days,
17006        });
17007        secret.last_changed_at = Utc::now();
17008        Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
17009    }
17010
17011    fn delete_secrets_manager_rotation_schedule(&self, physical_id: &str) -> Result<(), String> {
17012        let mut accounts = self.secretsmanager_state.write();
17013        let state = accounts.get_or_create(&self.account_id);
17014        if let Some(secret) = state.secrets.get_mut(physical_id) {
17015            secret.rotation_enabled = Some(false);
17016            secret.rotation_lambda_arn = None;
17017            secret.rotation_rules = None;
17018            secret.last_changed_at = Utc::now();
17019        }
17020        Ok(())
17021    }
17022
17023    fn create_secrets_manager_resource_policy(
17024        &self,
17025        resource: &ResourceDefinition,
17026    ) -> Result<ProvisionResult, String> {
17027        let props = &resource.properties;
17028        let secret_id = props
17029            .get("SecretId")
17030            .and_then(|v| v.as_str())
17031            .ok_or("SecretId is required")?
17032            .to_string();
17033        let policy_doc = props
17034            .get("ResourcePolicy")
17035            .ok_or("ResourcePolicy is required")?;
17036        let policy_str = match policy_doc {
17037            serde_json::Value::String(s) => s.clone(),
17038            other => other.to_string(),
17039        };
17040        let mut accounts = self.secretsmanager_state.write();
17041        let state = accounts.get_or_create(&self.account_id);
17042        let secret_arn = if state.secrets.contains_key(&secret_id) {
17043            secret_id.clone()
17044        } else {
17045            let candidate = format!(
17046                "arn:aws:secretsmanager:{}:{}:secret:{}",
17047                state.region, state.account_id, secret_id
17048            );
17049            if state.secrets.contains_key(&candidate) {
17050                candidate
17051            } else {
17052                return Err(format!("Secret {secret_id} not yet provisioned"));
17053            }
17054        };
17055        let secret = state
17056            .secrets
17057            .get_mut(&secret_arn)
17058            .ok_or_else(|| format!("Secret {secret_arn} not found"))?;
17059        secret.resource_policy = Some(policy_str);
17060        secret.last_changed_at = Utc::now();
17061        Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
17062    }
17063
17064    fn delete_secrets_manager_resource_policy(&self, physical_id: &str) -> Result<(), String> {
17065        let mut accounts = self.secretsmanager_state.write();
17066        let state = accounts.get_or_create(&self.account_id);
17067        if let Some(secret) = state.secrets.get_mut(physical_id) {
17068            secret.resource_policy = None;
17069            secret.last_changed_at = Utc::now();
17070        }
17071        Ok(())
17072    }
17073
17074    fn create_secrets_manager_target_attachment(
17075        &self,
17076        resource: &ResourceDefinition,
17077    ) -> Result<ProvisionResult, String> {
17078        let props = &resource.properties;
17079        let secret_id = props
17080            .get("SecretId")
17081            .and_then(|v| v.as_str())
17082            .ok_or("SecretId is required")?
17083            .to_string();
17084        let target_type = props
17085            .get("TargetType")
17086            .and_then(|v| v.as_str())
17087            .ok_or("TargetType is required")?;
17088        let target_id = props
17089            .get("TargetId")
17090            .and_then(|v| v.as_str())
17091            .ok_or("TargetId is required")?;
17092        let mut accounts = self.secretsmanager_state.write();
17093        let state = accounts.get_or_create(&self.account_id);
17094        let secret_arn = if state.secrets.contains_key(&secret_id) {
17095            secret_id.clone()
17096        } else {
17097            let candidate = format!(
17098                "arn:aws:secretsmanager:{}:{}:secret:{}",
17099                state.region, state.account_id, secret_id
17100            );
17101            if state.secrets.contains_key(&candidate) {
17102                candidate
17103            } else {
17104                return Err(format!("Secret {secret_id} not yet provisioned"));
17105            }
17106        };
17107        let secret = state
17108            .secrets
17109            .get_mut(&secret_arn)
17110            .ok_or_else(|| format!("Secret {secret_arn} not found"))?;
17111        // Patch the AWSCURRENT version with engine/host/dbInstanceIdentifier
17112        // so it shows as "attached" via the RDS-style schema CFN expects.
17113        // If the secret has no version yet (created without SecretString or
17114        // GenerateSecretString), seed one — this matches CFN's behaviour of
17115        // making the attachment usable on its own.
17116        let now = Utc::now();
17117        if secret.current_version_id.is_none() {
17118            let version_id = Uuid::new_v4().to_string();
17119            secret.versions.insert(
17120                version_id.clone(),
17121                SecretVersion {
17122                    version_id: version_id.clone(),
17123                    secret_string: Some("{}".to_string()),
17124                    secret_binary: None,
17125                    stages: vec!["AWSCURRENT".to_string()],
17126                    created_at: now,
17127                },
17128            );
17129            secret.current_version_id = Some(version_id);
17130        }
17131        if let Some(version_id) = secret.current_version_id.clone() {
17132            if let Some(version) = secret.versions.get_mut(&version_id) {
17133                let mut existing: serde_json::Value = version
17134                    .secret_string
17135                    .as_deref()
17136                    .and_then(|s| serde_json::from_str(s).ok())
17137                    .unwrap_or_else(|| serde_json::json!({}));
17138                if let Some(obj) = existing.as_object_mut() {
17139                    let engine = match target_type {
17140                        "AWS::RDS::DBInstance" | "AWS::RDS::DBCluster" => "postgres",
17141                        _ => "unknown",
17142                    };
17143                    obj.entry("engine".to_string())
17144                        .or_insert(serde_json::json!(engine));
17145                    obj.insert("host".to_string(), serde_json::json!(target_id));
17146                    obj.entry("dbInstanceIdentifier".to_string())
17147                        .or_insert(serde_json::json!(target_id));
17148                }
17149                version.secret_string = Some(existing.to_string());
17150            }
17151        }
17152        secret.last_changed_at = now;
17153        Ok(ProvisionResult::new(secret_arn.clone()).with("SecretArn", secret_arn))
17154    }
17155
17156    // --- Athena ---
17157
17158    fn create_athena_work_group(
17159        &self,
17160        resource: &ResourceDefinition,
17161    ) -> Result<ProvisionResult, String> {
17162        let props = &resource.properties;
17163        let name = props
17164            .get("Name")
17165            .and_then(|v| v.as_str())
17166            .unwrap_or(&resource.logical_id)
17167            .to_string();
17168        let description = props
17169            .get("Description")
17170            .and_then(|v| v.as_str())
17171            .map(|s| s.to_string());
17172        let configuration = props.get("Configuration").cloned();
17173        let state_str = props
17174            .get("State")
17175            .and_then(|v| v.as_str())
17176            .unwrap_or("ENABLED");
17177        let tags = Self::parse_athena_tags(props.get("Tags"));
17178
17179        let mut accounts = self.athena_state.write();
17180        let account = accounts
17181            .accounts
17182            .entry(self.account_id.clone())
17183            .or_default();
17184        account.ensure_initialized();
17185        if account.work_groups.contains_key(&name) {
17186            return Err(format!("WorkGroup {name} already exists"));
17187        }
17188        let wg = WorkGroup {
17189            name: name.clone(),
17190            state: state_str.to_string(),
17191            description,
17192            configuration,
17193            creation_time: Utc::now(),
17194            engine_version: Some("AUTO".to_string()),
17195        };
17196        let arn = format!(
17197            "arn:aws:athena:{}:{}:workgroup/{}",
17198            self.region, self.account_id, name
17199        );
17200        account.work_groups.insert(name.clone(), wg);
17201        if !tags.is_empty() {
17202            account.tags.insert(arn.clone(), tags);
17203        }
17204        Ok(ProvisionResult::new(name.clone())
17205            .with("Arn", arn)
17206            .with("Name", name))
17207    }
17208
17209    fn delete_athena_work_group(&self, physical_id: &str) -> Result<(), String> {
17210        let mut accounts = self.athena_state.write();
17211        let account = accounts
17212            .accounts
17213            .entry(self.account_id.clone())
17214            .or_default();
17215        account.work_groups.remove(physical_id);
17216        Ok(())
17217    }
17218
17219    fn get_att_athena_work_group(&self, physical_id: &str, attribute: &str) -> Option<String> {
17220        let mut accounts = self.athena_state.write();
17221        let account = accounts
17222            .accounts
17223            .entry(self.account_id.clone())
17224            .or_default();
17225        let wg = account.work_groups.get(physical_id)?;
17226        match attribute {
17227            "Arn" => Some(format!(
17228                "arn:aws:athena:{}:{}:workgroup/{}",
17229                self.region, self.account_id, wg.name
17230            )),
17231            "Name" => Some(wg.name.clone()),
17232            _ => None,
17233        }
17234    }
17235
17236    fn create_athena_data_catalog(
17237        &self,
17238        resource: &ResourceDefinition,
17239    ) -> Result<ProvisionResult, String> {
17240        let props = &resource.properties;
17241        let name = props
17242            .get("Name")
17243            .and_then(|v| v.as_str())
17244            .unwrap_or(&resource.logical_id)
17245            .to_string();
17246        let cat_type = props
17247            .get("Type")
17248            .and_then(|v| v.as_str())
17249            .ok_or("Type is required")?
17250            .to_string();
17251        let description = props
17252            .get("Description")
17253            .and_then(|v| v.as_str())
17254            .map(|s| s.to_string());
17255        let parameters = props
17256            .get("Parameters")
17257            .and_then(|v| v.as_object())
17258            .map(|m| {
17259                m.iter()
17260                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
17261                    .collect()
17262            })
17263            .unwrap_or_default();
17264        let connection_type = props
17265            .get("ConnectionType")
17266            .and_then(|v| v.as_str())
17267            .map(|s| s.to_string());
17268        let tags = Self::parse_athena_tags(props.get("Tags"));
17269
17270        let mut accounts = self.athena_state.write();
17271        let account = accounts
17272            .accounts
17273            .entry(self.account_id.clone())
17274            .or_default();
17275        account.ensure_initialized();
17276        if account.data_catalogs.contains_key(&name) {
17277            return Err(format!("DataCatalog {name} already exists"));
17278        }
17279        let cat = DataCatalog {
17280            name: name.clone(),
17281            description,
17282            cat_type,
17283            parameters,
17284            status: "CREATE_COMPLETE".to_string(),
17285            connection_type,
17286            error: None,
17287        };
17288        let arn = format!(
17289            "arn:aws:athena:{}:{}:datacatalog/{}",
17290            self.region, self.account_id, name
17291        );
17292        account.data_catalogs.insert(name.clone(), cat);
17293        if !tags.is_empty() {
17294            account.tags.insert(arn.clone(), tags);
17295        }
17296        Ok(ProvisionResult::new(name.clone())
17297            .with("Arn", arn)
17298            .with("Name", name))
17299    }
17300
17301    fn delete_athena_data_catalog(&self, physical_id: &str) -> Result<(), String> {
17302        let mut accounts = self.athena_state.write();
17303        let account = accounts
17304            .accounts
17305            .entry(self.account_id.clone())
17306            .or_default();
17307        account.data_catalogs.remove(physical_id);
17308        Ok(())
17309    }
17310
17311    fn get_att_athena_data_catalog(&self, physical_id: &str, attribute: &str) -> Option<String> {
17312        let mut accounts = self.athena_state.write();
17313        let account = accounts
17314            .accounts
17315            .entry(self.account_id.clone())
17316            .or_default();
17317        let cat = account.data_catalogs.get(physical_id)?;
17318        match attribute {
17319            "Arn" => Some(format!(
17320                "arn:aws:athena:{}:{}:datacatalog/{}",
17321                self.region, self.account_id, cat.name
17322            )),
17323            "Name" => Some(cat.name.clone()),
17324            _ => None,
17325        }
17326    }
17327
17328    fn create_athena_named_query(
17329        &self,
17330        resource: &ResourceDefinition,
17331    ) -> Result<ProvisionResult, String> {
17332        let props = &resource.properties;
17333        let name = props
17334            .get("Name")
17335            .and_then(|v| v.as_str())
17336            .ok_or("Name is required")?
17337            .to_string();
17338        let database = props
17339            .get("Database")
17340            .and_then(|v| v.as_str())
17341            .ok_or("Database is required")?
17342            .to_string();
17343        let query_string = props
17344            .get("QueryString")
17345            .and_then(|v| v.as_str())
17346            .ok_or("QueryString is required")?
17347            .to_string();
17348        let description = props
17349            .get("Description")
17350            .and_then(|v| v.as_str())
17351            .map(|s| s.to_string());
17352        let work_group = props
17353            .get("WorkGroup")
17354            .and_then(|v| v.as_str())
17355            .unwrap_or("primary")
17356            .to_string();
17357
17358        let mut accounts = self.athena_state.write();
17359        let account = accounts
17360            .accounts
17361            .entry(self.account_id.clone())
17362            .or_default();
17363        account.ensure_initialized();
17364        if !account.work_groups.contains_key(&work_group) {
17365            return Err(format!("Workgroup {work_group} not found"));
17366        }
17367        let id = Uuid::new_v4().to_string();
17368        let nq = NamedQuery {
17369            named_query_id: id.clone(),
17370            name,
17371            description,
17372            database,
17373            query_string,
17374            work_group,
17375            last_used_at: None,
17376        };
17377        account.named_queries.insert(id.clone(), nq);
17378        Ok(ProvisionResult::new(id.clone()).with("NamedQueryId", id))
17379    }
17380
17381    fn delete_athena_named_query(&self, physical_id: &str) -> Result<(), String> {
17382        let mut accounts = self.athena_state.write();
17383        let account = accounts
17384            .accounts
17385            .entry(self.account_id.clone())
17386            .or_default();
17387        account.named_queries.remove(physical_id);
17388        Ok(())
17389    }
17390
17391    fn get_att_athena_named_query(&self, physical_id: &str, attribute: &str) -> Option<String> {
17392        let mut accounts = self.athena_state.write();
17393        let account = accounts
17394            .accounts
17395            .entry(self.account_id.clone())
17396            .or_default();
17397        let nq = account.named_queries.get(physical_id)?;
17398        match attribute {
17399            "NamedQueryId" => Some(nq.named_query_id.clone()),
17400            _ => None,
17401        }
17402    }
17403
17404    fn create_athena_prepared_statement(
17405        &self,
17406        resource: &ResourceDefinition,
17407    ) -> Result<ProvisionResult, String> {
17408        let props = &resource.properties;
17409        let statement_name = props
17410            .get("StatementName")
17411            .and_then(|v| v.as_str())
17412            .ok_or("StatementName is required")?
17413            .to_string();
17414        let work_group_name = props
17415            .get("WorkGroupName")
17416            .and_then(|v| v.as_str())
17417            .ok_or("WorkGroupName is required")?
17418            .to_string();
17419        let query_statement = props
17420            .get("QueryStatement")
17421            .and_then(|v| v.as_str())
17422            .ok_or("QueryStatement is required")?
17423            .to_string();
17424        let description = props
17425            .get("Description")
17426            .and_then(|v| v.as_str())
17427            .map(|s| s.to_string());
17428
17429        let mut accounts = self.athena_state.write();
17430        let account = accounts
17431            .accounts
17432            .entry(self.account_id.clone())
17433            .or_default();
17434        account.ensure_initialized();
17435        if !account.work_groups.contains_key(&work_group_name) {
17436            return Err(format!("Workgroup {work_group_name} not found"));
17437        }
17438        let key = (work_group_name.clone(), statement_name.clone());
17439        if account.prepared_statements.contains_key(&key) {
17440            return Err(format!(
17441                "PreparedStatement {statement_name} already exists in {work_group_name}"
17442            ));
17443        }
17444        let ps = PreparedStatement {
17445            statement_name: statement_name.clone(),
17446            work_group_name: work_group_name.clone(),
17447            query_statement,
17448            description,
17449            last_modified_time: Utc::now(),
17450        };
17451        let physical_id = format!("{work_group_name}|{statement_name}");
17452        account.prepared_statements.insert(key, ps);
17453        Ok(ProvisionResult::new(physical_id))
17454    }
17455
17456    fn delete_athena_prepared_statement(
17457        &self,
17458        physical_id: &str,
17459        _attrs: &BTreeMap<String, String>,
17460    ) -> Result<(), String> {
17461        let mut accounts = self.athena_state.write();
17462        let account = accounts
17463            .accounts
17464            .entry(self.account_id.clone())
17465            .or_default();
17466        let parts: Vec<&str> = physical_id.split('|').collect();
17467        if parts.len() != 2 {
17468            return Err(format!(
17469                "Invalid PreparedStatement physical id: {physical_id}"
17470            ));
17471        }
17472        let key = (parts[0].to_string(), parts[1].to_string());
17473        account.prepared_statements.remove(&key);
17474        Ok(())
17475    }
17476
17477    fn get_att_athena_prepared_statement(
17478        &self,
17479        physical_id: &str,
17480        attribute: &str,
17481    ) -> Option<String> {
17482        let mut accounts = self.athena_state.write();
17483        let account = accounts
17484            .accounts
17485            .entry(self.account_id.clone())
17486            .or_default();
17487        let parts: Vec<&str> = physical_id.split('|').collect();
17488        if parts.len() != 2 {
17489            return None;
17490        }
17491        let ps = account
17492            .prepared_statements
17493            .get(&(parts[0].to_string(), parts[1].to_string()))?;
17494        match attribute {
17495            "StatementName" => Some(ps.statement_name.clone()),
17496            "WorkGroupName" => Some(ps.work_group_name.clone()),
17497            _ => None,
17498        }
17499    }
17500
17501    fn parse_athena_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
17502        let mut out = BTreeMap::new();
17503        let Some(arr) = value.and_then(|v| v.as_array()) else {
17504            return out;
17505        };
17506        for tag in arr {
17507            if let (Some(k), Some(v)) = (
17508                tag.get("Key").and_then(|v| v.as_str()),
17509                tag.get("Value").and_then(|v| v.as_str()),
17510            ) {
17511                out.insert(k.to_string(), v.to_string());
17512            }
17513        }
17514        out
17515    }
17516
17517    fn create_glue_database(
17518        &self,
17519        resource: &ResourceDefinition,
17520    ) -> Result<ProvisionResult, String> {
17521        let props = &resource.properties;
17522        let input = props
17523            .get("DatabaseInput")
17524            .ok_or("DatabaseInput is required")?;
17525        let name = input
17526            .get("Name")
17527            .and_then(|v| v.as_str())
17528            .unwrap_or(&resource.logical_id)
17529            .to_string();
17530        let description = input
17531            .get("Description")
17532            .and_then(|v| v.as_str())
17533            .map(|s| s.to_string());
17534        let location_uri = input
17535            .get("LocationUri")
17536            .and_then(|v| v.as_str())
17537            .map(|s| s.to_string());
17538        let parameters = input
17539            .get("Parameters")
17540            .and_then(|v| v.as_object())
17541            .map(|m| {
17542                m.iter()
17543                    .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
17544                    .collect()
17545            })
17546            .unwrap_or_default();
17547
17548        let mut accounts = self.glue_state.write();
17549        let state = accounts.get_or_create(&self.account_id, &self.region);
17550        let dbs = state.dbs_in_mut(&self.region);
17551        if dbs.contains_key(&name) {
17552            return Err(format!("Database {name} already exists"));
17553        }
17554        dbs.insert(
17555            name.clone(),
17556            fakecloud_glue::Database {
17557                name: name.clone(),
17558                description,
17559                location_uri,
17560                parameters,
17561                created_at: Utc::now(),
17562                catalog_id: self.account_id.clone(),
17563                tables: BTreeMap::new(),
17564            },
17565        );
17566        Ok(ProvisionResult::new(name.clone()))
17567    }
17568
17569    fn delete_glue_database(&self, physical_id: &str) -> Result<(), String> {
17570        let mut accounts = self.glue_state.write();
17571        let state = accounts.get_or_create(&self.account_id, &self.region);
17572        state.dbs_in_mut(&self.region).remove(physical_id);
17573        Ok(())
17574    }
17575
17576    fn create_glue_table(&self, resource: &ResourceDefinition) -> Result<ProvisionResult, String> {
17577        let props = &resource.properties;
17578        let db_name = props
17579            .get("DatabaseName")
17580            .and_then(|v| v.as_str())
17581            .ok_or("DatabaseName is required")?
17582            .to_string();
17583        let input = props.get("TableInput").ok_or("TableInput is required")?;
17584        let name = input
17585            .get("Name")
17586            .and_then(|v| v.as_str())
17587            .ok_or("TableInput.Name is required")?
17588            .to_string();
17589        let now = Utc::now();
17590
17591        let mut accounts = self.glue_state.write();
17592        let state = accounts.get_or_create(&self.account_id, &self.region);
17593        let dbs = state.dbs_in_mut(&self.region);
17594        let db = dbs
17595            .get_mut(&db_name)
17596            .ok_or_else(|| format!("Database {db_name} not found"))?;
17597        if db.tables.contains_key(&name) {
17598            return Err(format!("Table {name} already exists"));
17599        }
17600        db.tables.insert(
17601            name.clone(),
17602            fakecloud_glue::Table {
17603                name: name.clone(),
17604                database_name: db_name.clone(),
17605                description: input
17606                    .get("Description")
17607                    .and_then(|v| v.as_str())
17608                    .map(|s| s.to_string()),
17609                owner: input
17610                    .get("Owner")
17611                    .and_then(|v| v.as_str())
17612                    .map(|s| s.to_string()),
17613                create_time: now,
17614                update_time: now,
17615                last_access_time: None,
17616                retention: input.get("Retention").and_then(|v| v.as_i64()).unwrap_or(0),
17617                storage_descriptor: None,
17618                partition_keys: Vec::new(),
17619                view_original_text: input
17620                    .get("ViewOriginalText")
17621                    .and_then(|v| v.as_str())
17622                    .map(|s| s.to_string()),
17623                view_expanded_text: input
17624                    .get("ViewExpandedText")
17625                    .and_then(|v| v.as_str())
17626                    .map(|s| s.to_string()),
17627                table_type: input
17628                    .get("TableType")
17629                    .and_then(|v| v.as_str())
17630                    .map(|s| s.to_string()),
17631                parameters: BTreeMap::new(),
17632                partitions: BTreeMap::new(),
17633            },
17634        );
17635        let physical_id = format!("{db_name}|{name}");
17636        Ok(ProvisionResult::new(physical_id))
17637    }
17638
17639    fn delete_glue_table(&self, physical_id: &str) -> Result<(), String> {
17640        let mut accounts = self.glue_state.write();
17641        let state = accounts.get_or_create(&self.account_id, &self.region);
17642        let parts: Vec<&str> = physical_id.split('|').collect();
17643        if parts.len() != 2 {
17644            return Err(format!("Invalid Glue table physical id: {physical_id}"));
17645        }
17646        let dbs = state.dbs_in_mut(&self.region);
17647        let db = dbs
17648            .get_mut(parts[0])
17649            .ok_or_else(|| format!("Database {} not found", parts[0]))?;
17650        db.tables.remove(parts[1]);
17651        Ok(())
17652    }
17653
17654    fn create_glue_partition(
17655        &self,
17656        resource: &ResourceDefinition,
17657    ) -> Result<ProvisionResult, String> {
17658        let props = &resource.properties;
17659        let db_name = props
17660            .get("DatabaseName")
17661            .and_then(|v| v.as_str())
17662            .ok_or("DatabaseName is required")?
17663            .to_string();
17664        let table_name = props
17665            .get("TableName")
17666            .and_then(|v| v.as_str())
17667            .ok_or("TableName is required")?
17668            .to_string();
17669        let input = props
17670            .get("PartitionInput")
17671            .ok_or("PartitionInput is required")?;
17672        let values: Vec<String> = input
17673            .get("Values")
17674            .and_then(|v| v.as_array())
17675            .map(|arr| {
17676                arr.iter()
17677                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
17678                    .collect()
17679            })
17680            .unwrap_or_default();
17681        if values.is_empty() {
17682            return Err("PartitionInput.Values is required".to_string());
17683        }
17684        let key = values
17685            .iter()
17686            .map(|v| {
17687                v.replace('%', "%25")
17688                    .replace('|', "%7C")
17689                    .replace('/', "%2F")
17690            })
17691            .collect::<Vec<_>>()
17692            .join("/");
17693
17694        let mut accounts = self.glue_state.write();
17695        let state = accounts.get_or_create(&self.account_id, &self.region);
17696        let dbs = state.dbs_in_mut(&self.region);
17697        let db = dbs
17698            .get_mut(&db_name)
17699            .ok_or_else(|| format!("Database {db_name} not found"))?;
17700        let table = db
17701            .tables
17702            .get_mut(&table_name)
17703            .ok_or_else(|| format!("Table {table_name} not found"))?;
17704        if table.partitions.contains_key(&key) {
17705            return Err(format!("Partition {key} already exists"));
17706        }
17707        table.partitions.insert(
17708            key.clone(),
17709            fakecloud_glue::Partition {
17710                values: values.clone(),
17711                database_name: db_name.clone(),
17712                table_name: table_name.clone(),
17713                create_time: Utc::now(),
17714                last_access_time: None,
17715                storage_descriptor: None,
17716                parameters: BTreeMap::new(),
17717            },
17718        );
17719        let physical_id = format!("{db_name}|{table_name}|{key}");
17720        Ok(ProvisionResult::new(physical_id))
17721    }
17722
17723    fn delete_glue_partition(
17724        &self,
17725        physical_id: &str,
17726        _attrs: &BTreeMap<String, String>,
17727    ) -> Result<(), String> {
17728        let mut accounts = self.glue_state.write();
17729        let state = accounts.get_or_create(&self.account_id, &self.region);
17730        let parts: Vec<&str> = physical_id.split('|').collect();
17731        if parts.len() != 3 {
17732            return Err(format!("Invalid Glue partition physical id: {physical_id}"));
17733        }
17734        let dbs = state.dbs_in_mut(&self.region);
17735        let db = dbs
17736            .get_mut(parts[0])
17737            .ok_or_else(|| format!("Database {} not found", parts[0]))?;
17738        let table = db
17739            .tables
17740            .get_mut(parts[1])
17741            .ok_or_else(|| format!("Table {} not found", parts[1]))?;
17742        table.partitions.remove(parts[2]);
17743        Ok(())
17744    }
17745
17746    // --- CloudFormation Nested Stack ---
17747
17748    fn create_cloudformation_stack(
17749        &self,
17750        resource: &ResourceDefinition,
17751    ) -> Result<ProvisionResult, String> {
17752        let props = &resource.properties;
17753
17754        let template_body = if let Some(body) = props.get("TemplateBody").and_then(|v| v.as_str()) {
17755            body.to_string()
17756        } else if let Some(url) = props.get("TemplateURL").and_then(|v| v.as_str()) {
17757            self.fetch_template_from_url(url)?
17758        } else {
17759            return Err(
17760                "AWS::CloudFormation::Stack requires TemplateURL or TemplateBody".to_string(),
17761            );
17762        };
17763
17764        let child_parameters =
17765            if let Some(params) = props.get("Parameters").and_then(|v| v.as_object()) {
17766                let mut map = BTreeMap::new();
17767                for (k, v) in params {
17768                    if let Some(s) = v.as_str() {
17769                        map.insert(k.clone(), s.to_string());
17770                    } else {
17771                        map.insert(k.clone(), v.to_string());
17772                    }
17773                }
17774                map
17775            } else {
17776                BTreeMap::new()
17777            };
17778
17779        let child_tags = if let Some(tags) = props.get("Tags").and_then(|v| v.as_object()) {
17780            let mut map = BTreeMap::new();
17781            for (k, v) in tags {
17782                if let Some(s) = v.as_str() {
17783                    map.insert(k.clone(), s.to_string());
17784                }
17785            }
17786            map
17787        } else {
17788            BTreeMap::new()
17789        };
17790
17791        let parsed = crate::template::parse_template(&template_body, &child_parameters)
17792            .map_err(|e| format!("Failed to parse nested template: {e}"))?;
17793
17794        let child_stack_name = format!(
17795            "{}-Nested-{}",
17796            resource.logical_id,
17797            std::time::SystemTime::now()
17798                .duration_since(std::time::UNIX_EPOCH)
17799                .map(|d| d.as_nanos())
17800                .unwrap_or(0)
17801        );
17802        let child_stack_id = format!(
17803            "arn:aws:cloudformation:{}:{}:stack/{}/{}",
17804            self.region,
17805            self.account_id,
17806            child_stack_name,
17807            Uuid::new_v4()
17808        );
17809
17810        let child_provisioner = ResourceProvisioner {
17811            sqs_state: self.sqs_state.clone(),
17812            sns_state: self.sns_state.clone(),
17813            ssm_state: self.ssm_state.clone(),
17814            iam_state: self.iam_state.clone(),
17815            s3_state: self.s3_state.clone(),
17816            eventbridge_state: self.eventbridge_state.clone(),
17817            dynamodb_state: self.dynamodb_state.clone(),
17818            logs_state: self.logs_state.clone(),
17819            lambda_state: self.lambda_state.clone(),
17820            secretsmanager_state: self.secretsmanager_state.clone(),
17821            kinesis_state: self.kinesis_state.clone(),
17822            kms_state: self.kms_state.clone(),
17823            ecr_state: self.ecr_state.clone(),
17824            cloudwatch_state: self.cloudwatch_state.clone(),
17825            elbv2_state: self.elbv2_state.clone(),
17826            organizations_state: self.organizations_state.clone(),
17827            cognito_state: self.cognito_state.clone(),
17828            rds_state: self.rds_state.clone(),
17829            ecs_state: self.ecs_state.clone(),
17830            acm_state: self.acm_state.clone(),
17831            elasticache_state: self.elasticache_state.clone(),
17832            route53_state: self.route53_state.clone(),
17833            cloudfront_state: self.cloudfront_state.clone(),
17834            stepfunctions_state: self.stepfunctions_state.clone(),
17835            wafv2_state: self.wafv2_state.clone(),
17836            apigateway_state: self.apigateway_state.clone(),
17837            apigatewayv2_state: self.apigatewayv2_state.clone(),
17838            ses_state: self.ses_state.clone(),
17839            app_autoscaling_state: self.app_autoscaling_state.clone(),
17840            athena_state: self.athena_state.clone(),
17841            firehose_state: self.firehose_state.clone(),
17842            glue_state: self.glue_state.clone(),
17843            cloudformation_state: self.cloudformation_state.clone(),
17844            delivery: self.delivery.clone(),
17845            account_id: self.account_id.clone(),
17846            region: self.region.clone(),
17847            stack_id: child_stack_id.clone(),
17848        };
17849
17850        let child_resources = crate::service::provision_stack_resources(
17851            &child_provisioner,
17852            &parsed.resources,
17853            &template_body,
17854            &child_parameters,
17855        )
17856        .map_err(|e| format!("Failed to provision nested stack: {e}"))?;
17857
17858        let child_outputs = parsed.outputs;
17859
17860        let stack = crate::state::Stack {
17861            name: child_stack_name.clone(),
17862            stack_id: child_stack_id.clone(),
17863            template: template_body.clone(),
17864            status: "CREATE_COMPLETE".to_string(),
17865            resources: child_resources.clone(),
17866            parameters: child_parameters,
17867            tags: child_tags,
17868            created_at: Utc::now(),
17869            updated_at: None,
17870            description: parsed.description,
17871            notification_arns: Vec::new(),
17872            outputs: child_outputs
17873                .iter()
17874                .map(|o| crate::state::StackOutput {
17875                    key: o.logical_id.clone(),
17876                    value: o.value.clone(),
17877                    description: o.description.clone(),
17878                    export_name: o.export_name.clone(),
17879                })
17880                .collect(),
17881        };
17882
17883        {
17884            let mut accounts = self.cloudformation_state.write();
17885            let state = accounts.get_or_create(&self.account_id);
17886            state.stacks.insert(child_stack_name.clone(), stack);
17887
17888            crate::service::record_stack_status_event(
17889                state,
17890                &child_stack_id,
17891                &child_stack_name,
17892                "AWS::CloudFormation::Stack",
17893                "CREATE_IN_PROGRESS",
17894            );
17895            let changes: Vec<crate::service::ResourceChange> = child_resources
17896                .iter()
17897                .map(|r| crate::service::ResourceChange {
17898                    action: crate::service::ResourceChangeAction::Create,
17899                    logical_id: r.logical_id.clone(),
17900                    physical_id: r.physical_id.clone(),
17901                    resource_type: r.resource_type.clone(),
17902                })
17903                .collect();
17904            crate::service::record_stack_events(
17905                state,
17906                &child_stack_id,
17907                &child_stack_name,
17908                &changes,
17909            );
17910            crate::service::record_stack_status_event(
17911                state,
17912                &child_stack_id,
17913                &child_stack_name,
17914                "AWS::CloudFormation::Stack",
17915                "CREATE_COMPLETE",
17916            );
17917        }
17918
17919        let mut result = ProvisionResult::new(child_stack_id.clone());
17920        for output in &child_outputs {
17921            result.attributes.insert(
17922                format!("Outputs.{}", output.logical_id),
17923                output.value.clone(),
17924            );
17925        }
17926        Ok(result)
17927    }
17928
17929    fn delete_cloudformation_stack(&self, physical_id: &str) -> Result<(), String> {
17930        let stack = {
17931            let accounts = self.cloudformation_state.read();
17932            let state = accounts.get(&self.account_id);
17933            state.and_then(|s| {
17934                s.stacks
17935                    .values()
17936                    .find(|st| st.stack_id == physical_id)
17937                    .cloned()
17938            })
17939        };
17940
17941        if let Some(stack) = stack {
17942            let stack_name = stack.name.clone();
17943            let stack_id = stack.stack_id.clone();
17944
17945            for resource in stack.resources.iter().rev() {
17946                let _ = self.delete_resource(resource);
17947            }
17948
17949            {
17950                let mut accounts = self.cloudformation_state.write();
17951                let state = accounts.get_or_create(&self.account_id);
17952                state.stacks.remove(&stack_name);
17953
17954                crate::service::record_stack_status_event(
17955                    state,
17956                    &stack_id,
17957                    &stack_name,
17958                    "AWS::CloudFormation::Stack",
17959                    "DELETE_IN_PROGRESS",
17960                );
17961                crate::service::record_stack_status_event(
17962                    state,
17963                    &stack_id,
17964                    &stack_name,
17965                    "AWS::CloudFormation::Stack",
17966                    "DELETE_COMPLETE",
17967                );
17968            }
17969        }
17970
17971        Ok(())
17972    }
17973
17974    fn get_att_cloudformation_stack(&self, physical_id: &str, attribute: &str) -> Option<String> {
17975        let accounts = self.cloudformation_state.read();
17976        let state = accounts.get(&self.account_id)?;
17977        let stack = state.stacks.values().find(|s| s.stack_id == physical_id)?;
17978
17979        if let Some(output_key) = attribute.strip_prefix("Outputs.") {
17980            return stack
17981                .outputs
17982                .iter()
17983                .find(|o| o.key == output_key)
17984                .map(|o| o.value.clone());
17985        }
17986
17987        match attribute {
17988            "Outputs" => Some(
17989                serde_json::to_string(
17990                    &stack
17991                        .outputs
17992                        .iter()
17993                        .map(|o| (o.key.clone(), o.value.clone()))
17994                        .collect::<std::collections::BTreeMap<String, String>>(),
17995                )
17996                .unwrap_or_default(),
17997            ),
17998            _ => None,
17999        }
18000    }
18001
18002    fn fetch_template_from_url(&self, url: &str) -> Result<String, String> {
18003        if let Some(rest) = url.strip_prefix("s3://") {
18004            let parts: Vec<&str> = rest.splitn(2, '/').collect();
18005            if parts.len() != 2 {
18006                return Err("Invalid s3:// URL".to_string());
18007            }
18008            return self.fetch_s3_template(parts[0], parts[1]);
18009        }
18010
18011        if let Some(rest) = url.strip_prefix("https://s3.amazonaws.com/") {
18012            let parts: Vec<&str> = rest.splitn(2, '/').collect();
18013            if parts.len() != 2 {
18014                return Err("Invalid S3 HTTPS URL".to_string());
18015            }
18016            return self.fetch_s3_template(parts[0], parts[1]);
18017        }
18018
18019        if let Some(host_rest) = url.strip_prefix("https://") {
18020            if let Some(slash_pos) = host_rest.find('/') {
18021                let host = &host_rest[..slash_pos];
18022                let key = &host_rest[slash_pos + 1..];
18023                if let Some(bucket) = host.strip_suffix(".s3.amazonaws.com") {
18024                    return self.fetch_s3_template(bucket, key);
18025                }
18026                if host.contains(".s3.") && host.ends_with(".amazonaws.com") {
18027                    let bucket = host.split(".s3.").next().unwrap_or("");
18028                    if !bucket.is_empty() {
18029                        return self.fetch_s3_template(bucket, key);
18030                    }
18031                }
18032            }
18033        }
18034
18035        Err(format!("Unsupported TemplateURL: {url}"))
18036    }
18037
18038    fn fetch_s3_template(&self, bucket: &str, key: &str) -> Result<String, String> {
18039        let mut s3_accounts = self.s3_state.write();
18040        let s3_state = s3_accounts.get_or_create(&self.account_id);
18041        let bucket_obj = s3_state
18042            .buckets
18043            .get(bucket)
18044            .ok_or_else(|| format!("S3 bucket not found: {bucket}"))?;
18045        let obj = bucket_obj
18046            .objects
18047            .get(key)
18048            .ok_or_else(|| format!("S3 object not found: {bucket}/{key}"))?;
18049        let bytes = s3_state
18050            .read_body(&obj.body)
18051            .map_err(|e| format!("Failed to read S3 object body: {e}"))?;
18052        String::from_utf8(bytes.to_vec()).map_err(|e| format!("S3 object is not valid UTF-8: {e}"))
18053    }
18054}
18055
18056/// Implements the CloudFormation `GenerateSecretString` shape on
18057/// `AWS::SecretsManager::Secret`. Produces the plaintext payload that
18058/// will become the AWSCURRENT version of the secret.
18059///
18060/// When `SecretStringTemplate` is set together with `GenerateStringKey`,
18061/// the generated password is inserted under `GenerateStringKey` in the
18062/// JSON template (this is the standard "DB credential" pattern).
18063/// Without those two, the bare generated password is returned.
18064fn generate_secret_string_payload(gen: &serde_json::Value) -> Result<String, String> {
18065    let length = gen
18066        .get("PasswordLength")
18067        .and_then(|v| v.as_i64())
18068        .unwrap_or(32) as usize;
18069    let exclude_lowercase = gen
18070        .get("ExcludeLowercase")
18071        .and_then(|v| v.as_bool())
18072        .unwrap_or(false);
18073    let exclude_uppercase = gen
18074        .get("ExcludeUppercase")
18075        .and_then(|v| v.as_bool())
18076        .unwrap_or(false);
18077    let exclude_numbers = gen
18078        .get("ExcludeNumbers")
18079        .and_then(|v| v.as_bool())
18080        .unwrap_or(false);
18081    let exclude_punctuation = gen
18082        .get("ExcludePunctuation")
18083        .and_then(|v| v.as_bool())
18084        .unwrap_or(false);
18085    let include_space = gen
18086        .get("IncludeSpace")
18087        .and_then(|v| v.as_bool())
18088        .unwrap_or(false);
18089    let exclude_chars = gen
18090        .get("ExcludeCharacters")
18091        .and_then(|v| v.as_str())
18092        .unwrap_or("")
18093        .to_string();
18094
18095    let lowercase = "abcdefghijklmnopqrstuvwxyz";
18096    let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
18097    let digits = "0123456789";
18098    let punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
18099
18100    let mut pool = String::new();
18101    if !exclude_lowercase {
18102        pool.extend(lowercase.chars().filter(|c| !exclude_chars.contains(*c)));
18103    }
18104    if !exclude_uppercase {
18105        pool.extend(uppercase.chars().filter(|c| !exclude_chars.contains(*c)));
18106    }
18107    if !exclude_numbers {
18108        pool.extend(digits.chars().filter(|c| !exclude_chars.contains(*c)));
18109    }
18110    if !exclude_punctuation {
18111        pool.extend(punctuation.chars().filter(|c| !exclude_chars.contains(*c)));
18112    }
18113    if include_space && !exclude_chars.contains(' ') {
18114        pool.push(' ');
18115    }
18116    if pool.is_empty() {
18117        return Err("GenerateSecretString character pool is empty".to_string());
18118    }
18119
18120    let pool_chars: Vec<char> = pool.chars().collect();
18121    let mut password = String::with_capacity(length);
18122    let mut counter: u64 = std::time::SystemTime::now()
18123        .duration_since(std::time::UNIX_EPOCH)
18124        .map(|d| d.as_nanos() as u64)
18125        .unwrap_or(0);
18126    while password.len() < length {
18127        // Splitmix64 — deterministic, no_std-friendly, good enough for
18128        // fake-cloud password generation. Real entropy isn't a goal here.
18129        counter = counter.wrapping_add(0x9E3779B97F4A7C15);
18130        let mut z = counter;
18131        z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
18132        z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
18133        z ^= z >> 31;
18134        let idx = (z as usize) % pool_chars.len();
18135        password.push(pool_chars[idx]);
18136    }
18137
18138    let template = gen.get("SecretStringTemplate").and_then(|v| v.as_str());
18139    let key = gen.get("GenerateStringKey").and_then(|v| v.as_str());
18140    match (template, key) {
18141        (Some(tmpl), Some(k)) => {
18142            let mut value: serde_json::Value = serde_json::from_str(tmpl)
18143                .map_err(|e| format!("SecretStringTemplate is not valid JSON: {e}"))?;
18144            if let Some(obj) = value.as_object_mut() {
18145                obj.insert(k.to_string(), serde_json::Value::String(password));
18146                Ok(value.to_string())
18147            } else {
18148                Err("SecretStringTemplate must be a JSON object".to_string())
18149            }
18150        }
18151        _ => Ok(password),
18152    }
18153}
18154
18155fn parse_ses_receipt_action(value: &serde_json::Value) -> Option<SesReceiptAction> {
18156    let obj = value.as_object()?;
18157    if let Some(s3) = obj.get("S3Action").and_then(|v| v.as_object()) {
18158        let bucket_name = s3.get("BucketName").and_then(|v| v.as_str())?.to_string();
18159        return Some(SesReceiptAction::S3 {
18160            bucket_name,
18161            object_key_prefix: s3
18162                .get("ObjectKeyPrefix")
18163                .and_then(|v| v.as_str())
18164                .map(String::from),
18165            topic_arn: s3
18166                .get("TopicArn")
18167                .and_then(|v| v.as_str())
18168                .map(String::from),
18169            kms_key_arn: s3
18170                .get("KmsKeyArn")
18171                .and_then(|v| v.as_str())
18172                .map(String::from),
18173        });
18174    }
18175    if let Some(sns) = obj.get("SNSAction").and_then(|v| v.as_object()) {
18176        return Some(SesReceiptAction::Sns {
18177            topic_arn: sns.get("TopicArn").and_then(|v| v.as_str())?.to_string(),
18178            encoding: sns
18179                .get("Encoding")
18180                .and_then(|v| v.as_str())
18181                .map(String::from),
18182        });
18183    }
18184    if let Some(la) = obj.get("LambdaAction").and_then(|v| v.as_object()) {
18185        return Some(SesReceiptAction::Lambda {
18186            function_arn: la.get("FunctionArn").and_then(|v| v.as_str())?.to_string(),
18187            invocation_type: la
18188                .get("InvocationType")
18189                .and_then(|v| v.as_str())
18190                .map(String::from),
18191            topic_arn: la
18192                .get("TopicArn")
18193                .and_then(|v| v.as_str())
18194                .map(String::from),
18195        });
18196    }
18197    if let Some(b) = obj.get("BounceAction").and_then(|v| v.as_object()) {
18198        return Some(SesReceiptAction::Bounce {
18199            smtp_reply_code: b
18200                .get("SmtpReplyCode")
18201                .and_then(|v| v.as_str())
18202                .unwrap_or("550")
18203                .to_string(),
18204            message: b
18205                .get("Message")
18206                .and_then(|v| v.as_str())
18207                .unwrap_or("")
18208                .to_string(),
18209            sender: b
18210                .get("Sender")
18211                .and_then(|v| v.as_str())
18212                .unwrap_or("")
18213                .to_string(),
18214            status_code: b
18215                .get("StatusCode")
18216                .and_then(|v| v.as_str())
18217                .map(String::from),
18218            topic_arn: b.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
18219        });
18220    }
18221    if let Some(ah) = obj.get("AddHeaderAction").and_then(|v| v.as_object()) {
18222        return Some(SesReceiptAction::AddHeader {
18223            header_name: ah.get("HeaderName").and_then(|v| v.as_str())?.to_string(),
18224            header_value: ah.get("HeaderValue").and_then(|v| v.as_str())?.to_string(),
18225        });
18226    }
18227    if let Some(s) = obj.get("StopAction").and_then(|v| v.as_object()) {
18228        return Some(SesReceiptAction::Stop {
18229            scope: s
18230                .get("Scope")
18231                .and_then(|v| v.as_str())
18232                .unwrap_or("RuleSet")
18233                .to_string(),
18234            topic_arn: s.get("TopicArn").and_then(|v| v.as_str()).map(String::from),
18235        });
18236    }
18237    None
18238}
18239
18240/// Generate an N-character alphanumeric id for API Gateway v2 resources.
18241/// AWS HTTP API ids are 10 chars; routes/integrations/etc are similarly
18242/// short. This mirrors the runtime crate's id shape.
18243fn make_apigwv2_id(n: usize) -> String {
18244    let s = uuid::Uuid::new_v4().simple().to_string();
18245    s[..n.min(s.len())].to_string()
18246}
18247
18248/// Lowercase the first letter of each key in a JSON object, recursively.
18249/// CloudFormation property names are PascalCase (`BurstLimit`,
18250/// `RateLimit`); the runtime API Gateway service stores values keyed in
18251/// camelCase (`burstLimit`, `rateLimit`). Used at the CFN/service
18252/// boundary so JSON pulled from the template can flow into the runtime
18253/// state without renaming each leaf by hand.
18254/// Coerce a CFN-resolved Number/String parameter into i64. CFN
18255/// parameters surface here as `Value::String` after `Ref` substitution,
18256/// even when the parameter `Type` is `Number`, so we need both paths.
18257fn cfn_as_i64(v: &serde_json::Value) -> Option<i64> {
18258    if let Some(n) = v.as_i64() {
18259        return Some(n);
18260    }
18261    v.as_str().and_then(|s| s.parse::<i64>().ok())
18262}
18263
18264fn lowercase_first_keys(value: serde_json::Value) -> serde_json::Value {
18265    match value {
18266        serde_json::Value::Object(map) => {
18267            let mut out = serde_json::Map::new();
18268            for (k, v) in map {
18269                let new_key = if let Some(first) = k.chars().next() {
18270                    let mut s = String::with_capacity(k.len());
18271                    s.extend(first.to_lowercase());
18272                    s.push_str(&k[first.len_utf8()..]);
18273                    s
18274                } else {
18275                    k
18276                };
18277                out.insert(new_key, lowercase_first_keys(v));
18278            }
18279            serde_json::Value::Object(out)
18280        }
18281        serde_json::Value::Array(arr) => {
18282            serde_json::Value::Array(arr.into_iter().map(lowercase_first_keys).collect())
18283        }
18284        other => other,
18285    }
18286}
18287
18288/// Synthesize the per-domain DNS validation record list for a
18289/// CFN-provisioned cert. Mirrors the runtime ACM path: each name gets
18290/// a SUCCESS record (since CFN-issued certs are auto-validated above)
18291/// and an `_amzn-validations.<domain>.` resource record so callers
18292/// that read DescribeCertificate see the same shape they'd expect
18293/// from a real ACM-issued cert.
18294fn synth_acm_domain_validation(
18295    domain_name: &str,
18296    sans: &[String],
18297    validation_method: &str,
18298) -> Vec<AcmDomainValidation> {
18299    let mut all = vec![domain_name.to_string()];
18300    for s in sans {
18301        if !all.contains(s) {
18302            all.push(s.clone());
18303        }
18304    }
18305    all.into_iter()
18306        .map(|name| AcmDomainValidation {
18307            domain_name: name.clone(),
18308            validation_status: "SUCCESS".to_string(),
18309            validation_method: validation_method.to_string(),
18310            resource_record_name: Some(format!("_amzn-validations.{name}.")),
18311            resource_record_type: Some("CNAME".to_string()),
18312            resource_record_value: Some(format!("{}.acm-validations.aws.", Uuid::new_v4())),
18313        })
18314        .collect()
18315}
18316
18317/// Convert CFN `Tags` array into the ACM crate's tag map form.
18318fn parse_acm_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
18319    let mut out = BTreeMap::new();
18320    if let Some(arr) = value.and_then(|v| v.as_array()) {
18321        for t in arr {
18322            if let (Some(k), Some(v)) = (
18323                t.get("Key").and_then(|v| v.as_str()),
18324                t.get("Value").and_then(|v| v.as_str()),
18325            ) {
18326                out.insert(k.to_string(), v.to_string());
18327            }
18328        }
18329    }
18330    out
18331}
18332
18333/// Convert CFN `Tags` array into the ECS `TagEntry` form.
18334fn parse_ecs_tags(value: Option<&serde_json::Value>) -> Vec<EcsTagEntry> {
18335    let Some(arr) = value.and_then(|v| v.as_array()) else {
18336        return Vec::new();
18337    };
18338    arr.iter()
18339        .filter_map(|t| {
18340            let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
18341            let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
18342            Some(EcsTagEntry { key, value })
18343        })
18344        .collect()
18345}
18346
18347/// Strip the cluster ARN prefix when CFN passes a Ref-resolved name or
18348/// a GetAtt-resolved ARN; ECS internal state keys clusters by name.
18349fn parse_ecs_cluster_name(input: &str) -> String {
18350    if let Some(after) = input.split(":cluster/").nth(1) {
18351        return after.to_string();
18352    }
18353    input.to_string()
18354}
18355
18356/// Pull `(family, revision)` out of a task definition ARN tail like
18357/// `task-definition/web:3`. Returns `(family, revision)` or
18358/// `(input, 1)` for unrecognised shapes.
18359fn parse_td_arn(input: &str) -> (String, i32) {
18360    let suffix = input.rsplit('/').next().unwrap_or(input);
18361    if let Some((family, rev)) = suffix.split_once(':') {
18362        if let Ok(revision) = rev.parse::<i32>() {
18363            return (family.to_string(), revision);
18364        }
18365    }
18366    (input.to_string(), 1)
18367}
18368
18369/// Pull `(cluster, service)` out of a service ARN like
18370/// `arn:aws:ecs:us-east-1:000000000000:service/<cluster>/<service>`.
18371fn parse_service_arn(input: &str) -> Option<(String, String)> {
18372    let after = input.split(":service/").nth(1)?;
18373    let mut parts = after.splitn(2, '/');
18374    let cluster = parts.next()?.to_string();
18375    let service = parts.next()?.to_string();
18376    Some((cluster, service))
18377}
18378
18379/// Parse CFN-shape Tags array into the RDS crate's tag form.
18380fn parse_rds_tags(value: Option<&serde_json::Value>) -> Vec<RdsTag> {
18381    let Some(arr) = value.and_then(|v| v.as_array()) else {
18382        return Vec::new();
18383    };
18384    arr.iter()
18385        .filter_map(|t| {
18386            let key = t.get("Key").and_then(|v| v.as_str())?.to_string();
18387            let value = t.get("Value").and_then(|v| v.as_str())?.to_string();
18388            Some(RdsTag { key, value })
18389        })
18390        .collect()
18391}
18392
18393/// Lazy-create an entry in the RDS `extras` bucket so the provisioner
18394/// doesn't have to re-implement the `BTreeMap::entry` boilerplate per
18395/// resource type.
18396fn rds_extras_mut<'a>(
18397    state: &'a mut fakecloud_rds::RdsState,
18398    category: &str,
18399) -> &'a mut BTreeMap<String, serde_json::Value> {
18400    state.extras.entry(category.to_string()).or_default()
18401}
18402
18403/// Parse a JSON array-of-strings property. Returns empty Vec when the
18404/// value is missing or shaped wrong; matches the tolerant input handling
18405/// used by the runtime Cognito service.
18406fn parse_cognito_string_array(value: Option<&serde_json::Value>) -> Vec<String> {
18407    value
18408        .and_then(|v| v.as_array())
18409        .map(|arr| {
18410            arr.iter()
18411                .filter_map(|v| v.as_str().map(|s| s.to_string()))
18412                .collect()
18413        })
18414        .unwrap_or_default()
18415}
18416
18417fn parse_cognito_password_policy(value: Option<&serde_json::Value>) -> PasswordPolicy {
18418    let Some(inner) = value
18419        .and_then(|v| v.get("PasswordPolicy"))
18420        .and_then(|v| v.as_object())
18421    else {
18422        return PasswordPolicy::default();
18423    };
18424    let mut p = PasswordPolicy::default();
18425    if let Some(n) = inner.get("MinimumLength").and_then(|v| v.as_i64()) {
18426        p.minimum_length = n;
18427    }
18428    if let Some(b) = inner.get("RequireUppercase").and_then(|v| v.as_bool()) {
18429        p.require_uppercase = b;
18430    }
18431    if let Some(b) = inner.get("RequireLowercase").and_then(|v| v.as_bool()) {
18432        p.require_lowercase = b;
18433    }
18434    if let Some(b) = inner.get("RequireNumbers").and_then(|v| v.as_bool()) {
18435        p.require_numbers = b;
18436    }
18437    if let Some(b) = inner.get("RequireSymbols").and_then(|v| v.as_bool()) {
18438        p.require_symbols = b;
18439    }
18440    if let Some(n) = inner
18441        .get("TemporaryPasswordValidityDays")
18442        .and_then(|v| v.as_i64())
18443    {
18444        p.temporary_password_validity_days = n;
18445    }
18446    p
18447}
18448
18449fn parse_cognito_schema_attribute(value: &serde_json::Value) -> Option<SchemaAttribute> {
18450    let name = value.get("Name").and_then(|v| v.as_str())?.to_string();
18451    Some(SchemaAttribute {
18452        name,
18453        attribute_data_type: value
18454            .get("AttributeDataType")
18455            .and_then(|v| v.as_str())
18456            .unwrap_or("String")
18457            .to_string(),
18458        developer_only_attribute: value
18459            .get("DeveloperOnlyAttribute")
18460            .and_then(|v| v.as_bool())
18461            .unwrap_or(false),
18462        mutable: value
18463            .get("Mutable")
18464            .and_then(|v| v.as_bool())
18465            .unwrap_or(true),
18466        required: value
18467            .get("Required")
18468            .and_then(|v| v.as_bool())
18469            .unwrap_or(false),
18470        string_attribute_constraints: None,
18471        number_attribute_constraints: None,
18472    })
18473}
18474
18475fn parse_cognito_tags(value: Option<&serde_json::Value>) -> BTreeMap<String, String> {
18476    let mut out = BTreeMap::new();
18477    if let Some(obj) = value.and_then(|v| v.as_object()) {
18478        for (k, v) in obj {
18479            if let Some(s) = v.as_str() {
18480                out.insert(k.clone(), s.to_string());
18481            }
18482        }
18483    }
18484    out
18485}
18486
18487fn parse_cognito_email_configuration(
18488    value: Option<&serde_json::Value>,
18489) -> Option<EmailConfiguration> {
18490    let inner = value?.as_object()?;
18491    Some(EmailConfiguration {
18492        source_arn: inner
18493            .get("SourceArn")
18494            .and_then(|v| v.as_str())
18495            .map(|s| s.to_string()),
18496        reply_to_email_address: inner
18497            .get("ReplyToEmailAddress")
18498            .and_then(|v| v.as_str())
18499            .map(|s| s.to_string()),
18500        email_sending_account: inner
18501            .get("EmailSendingAccount")
18502            .and_then(|v| v.as_str())
18503            .map(|s| s.to_string()),
18504        from_email_address: inner
18505            .get("From")
18506            .and_then(|v| v.as_str())
18507            .map(|s| s.to_string()),
18508        configuration_set: inner
18509            .get("ConfigurationSet")
18510            .and_then(|v| v.as_str())
18511            .map(|s| s.to_string()),
18512    })
18513}
18514
18515fn parse_cognito_sms_configuration(value: Option<&serde_json::Value>) -> Option<SmsConfiguration> {
18516    let inner = value?.as_object()?;
18517    Some(SmsConfiguration {
18518        sns_caller_arn: inner
18519            .get("SnsCallerArn")
18520            .and_then(|v| v.as_str())
18521            .map(|s| s.to_string()),
18522        external_id: inner
18523            .get("ExternalId")
18524            .and_then(|v| v.as_str())
18525            .map(|s| s.to_string()),
18526        sns_region: inner
18527            .get("SnsRegion")
18528            .and_then(|v| v.as_str())
18529            .map(|s| s.to_string()),
18530    })
18531}
18532
18533fn parse_cognito_admin_create_user_config(
18534    value: Option<&serde_json::Value>,
18535) -> Option<AdminCreateUserConfig> {
18536    let inner = value?.as_object()?;
18537    Some(AdminCreateUserConfig {
18538        allow_admin_create_user_only: inner
18539            .get("AllowAdminCreateUserOnly")
18540            .and_then(|v| v.as_bool()),
18541        invite_message_template: None,
18542        unused_account_validity_days: inner
18543            .get("UnusedAccountValidityDays")
18544            .and_then(|v| v.as_i64()),
18545    })
18546}
18547
18548fn parse_cognito_account_recovery(
18549    value: Option<&serde_json::Value>,
18550) -> Option<AccountRecoverySetting> {
18551    let arr = value?.get("RecoveryMechanisms")?.as_array()?;
18552    Some(AccountRecoverySetting {
18553        recovery_mechanisms: arr
18554            .iter()
18555            .filter_map(|m| {
18556                let name = m.get("Name").and_then(|v| v.as_str())?.to_string();
18557                let priority = m.get("Priority").and_then(|v| v.as_i64()).unwrap_or(1);
18558                Some(RecoveryOption { name, priority })
18559            })
18560            .collect(),
18561    })
18562}
18563
18564fn parse_firehose_s3_destination(value: &serde_json::Value) -> Result<S3Destination, String> {
18565    let role_arn = value
18566        .get("RoleARN")
18567        .and_then(|v| v.as_str())
18568        .ok_or("S3 destination requires RoleARN")?
18569        .to_string();
18570    let bucket_arn = value
18571        .get("BucketARN")
18572        .and_then(|v| v.as_str())
18573        .ok_or("S3 destination requires BucketARN")?
18574        .to_string();
18575    let prefix = value
18576        .get("Prefix")
18577        .and_then(|v| v.as_str())
18578        .map(|s| s.to_string());
18579    let error_output_prefix = value
18580        .get("ErrorOutputPrefix")
18581        .and_then(|v| v.as_str())
18582        .map(|s| s.to_string());
18583    let mut buffering_size_mb = None;
18584    let mut buffering_interval_seconds = None;
18585    if let Some(hints) = value.get("BufferingHints") {
18586        buffering_size_mb = hints.get("SizeInMBs").and_then(|v| v.as_i64());
18587        buffering_interval_seconds = hints.get("IntervalInSeconds").and_then(|v| v.as_i64());
18588    }
18589    let compression_format = value
18590        .get("CompressionFormat")
18591        .and_then(|v| v.as_str())
18592        .map(|s| s.to_string());
18593
18594    Ok(S3Destination {
18595        destination_id: "destination-1".to_string(),
18596        role_arn,
18597        bucket_arn,
18598        prefix,
18599        error_output_prefix,
18600        buffering_size_mb,
18601        buffering_interval_seconds,
18602        compression_format,
18603    })
18604}
18605
18606#[cfg(test)]
18607mod tests {
18608    use super::*;
18609    use parking_lot::RwLock;
18610
18611    fn make_provisioner() -> ResourceProvisioner {
18612        ResourceProvisioner {
18613            sqs_state: Arc::new(RwLock::new(
18614                fakecloud_core::multi_account::MultiAccountState::new(
18615                    "123456789012",
18616                    "us-east-1",
18617                    "http://localhost:4566",
18618                ),
18619            )),
18620            sns_state: Arc::new(RwLock::new(
18621                fakecloud_core::multi_account::MultiAccountState::new(
18622                    "123456789012",
18623                    "us-east-1",
18624                    "http://localhost:4566",
18625                ),
18626            )),
18627            ssm_state: Arc::new(RwLock::new(
18628                fakecloud_core::multi_account::MultiAccountState::new(
18629                    "123456789012",
18630                    "us-east-1",
18631                    "http://localhost:4566",
18632                ),
18633            )),
18634            iam_state: Arc::new(RwLock::new(
18635                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "http://localhost:4566"),
18636            )),
18637            s3_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
18638                "123456789012",
18639                "us-east-1", "",
18640            ))),
18641            eventbridge_state: Arc::new(RwLock::new(
18642                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18643            )),
18644            dynamodb_state: Arc::new(RwLock::new(fakecloud_core::multi_account::MultiAccountState::new(
18645                "123456789012",
18646                "us-east-1", "",
18647            ))),
18648            logs_state: Arc::new(RwLock::new(
18649                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18650            )),
18651            lambda_state: Arc::new(RwLock::new(
18652                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18653            )),
18654            secretsmanager_state: Arc::new(RwLock::new(
18655                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18656            )),
18657            kinesis_state: Arc::new(RwLock::new(
18658                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18659            )),
18660            kms_state: Arc::new(RwLock::new(
18661                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18662            )),
18663            ecr_state: Arc::new(RwLock::new(
18664                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18665            )),
18666            cloudwatch_state: Arc::new(RwLock::new(fakecloud_cloudwatch::CloudWatchAccounts::new())),
18667            elbv2_state: Arc::new(RwLock::new(fakecloud_elbv2::Elbv2Accounts::new())),
18668            organizations_state: Arc::new(RwLock::new(None)),
18669            cognito_state: Arc::new(RwLock::new(
18670                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18671            )),
18672            rds_state: Arc::new(RwLock::new(
18673                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18674            )),
18675            ecs_state: Arc::new(RwLock::new(
18676                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18677            )),
18678            acm_state: Arc::new(RwLock::new(fakecloud_acm::AcmAccounts::new())),
18679            elasticache_state: Arc::new(RwLock::new(
18680                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18681            )),
18682            route53_state: Arc::new(RwLock::new(fakecloud_route53::Route53Accounts::new())),
18683            cloudfront_state: Arc::new(RwLock::new(
18684                fakecloud_cloudfront::CloudFrontAccounts::new(),
18685            )),
18686            cloudformation_state: Arc::new(RwLock::new(
18687                fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
18688            )),
18689            stepfunctions_state: Arc::new(RwLock::new(
18690                fakecloud_core::multi_account::MultiAccountState::new(
18691                    "123456789012",
18692                    "us-east-1",
18693                    "",
18694                ),
18695            )),
18696            wafv2_state: Arc::new(RwLock::new(fakecloud_wafv2::Wafv2Accounts::default())),
18697            apigateway_state: Arc::new(RwLock::new(
18698                fakecloud_core::multi_account::MultiAccountState::new(
18699                    "123456789012",
18700                    "us-east-1",
18701                    "",
18702                ),
18703            )),
18704            apigatewayv2_state: Arc::new(RwLock::new(
18705                fakecloud_core::multi_account::MultiAccountState::new(
18706                    "123456789012",
18707                    "us-east-1",
18708                    "",
18709                ),
18710            )),
18711            ses_state: Arc::new(RwLock::new(
18712                fakecloud_core::multi_account::MultiAccountState::new(
18713                    "123456789012",
18714                    "us-east-1",
18715                    "",
18716                ),
18717            )),
18718            app_autoscaling_state: Arc::new(parking_lot::RwLock::new(
18719                fakecloud_application_autoscaling::ApplicationAutoScalingAccounts::new(),
18720            )),
18721            athena_state: Arc::new(parking_lot::RwLock::new(
18722                fakecloud_athena::AthenaAccounts::new(),
18723            )),
18724            firehose_state: Arc::new(parking_lot::RwLock::new(
18725                fakecloud_firehose::FirehoseAccounts::new(),
18726            )),
18727            glue_state: Arc::new(parking_lot::RwLock::new(
18728                fakecloud_glue::GlueAccounts::new(),
18729            )),
18730            delivery: Arc::new(DeliveryBus::new()),
18731            account_id: "123456789012".to_string(),
18732            region: "us-east-1".to_string(),
18733            stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test/00000000-0000-0000-0000-000000000000".to_string(),
18734        }
18735    }
18736
18737    fn make_resource(
18738        resource_type: &str,
18739        logical_id: &str,
18740        props: serde_json::Value,
18741    ) -> ResourceDefinition {
18742        ResourceDefinition {
18743            logical_id: logical_id.to_string(),
18744            resource_type: resource_type.to_string(),
18745            properties: props,
18746        }
18747    }
18748
18749    #[test]
18750    fn sns_subscription_rejects_nonexistent_topic() {
18751        let prov = make_provisioner();
18752        let resource = make_resource(
18753            "AWS::SNS::Subscription",
18754            "MySub",
18755            serde_json::json!({
18756                "TopicArn": "arn:aws:sns:us-east-1:123456789012:NonExistent",
18757                "Protocol": "sqs",
18758                "Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
18759            }),
18760        );
18761        let result = prov.create_resource(&resource);
18762        assert!(result.is_err());
18763        assert!(result.unwrap_err().contains("does not exist"));
18764    }
18765
18766    #[test]
18767    fn sns_subscription_succeeds_when_topic_exists() {
18768        let prov = make_provisioner();
18769        // First create the topic
18770        let topic = make_resource(
18771            "AWS::SNS::Topic",
18772            "MyTopic",
18773            serde_json::json!({ "TopicName": "my-topic" }),
18774        );
18775        let topic_result = prov.create_resource(&topic);
18776        assert!(topic_result.is_ok());
18777        let topic_arn = topic_result.unwrap().physical_id;
18778
18779        // Now create subscription referencing that topic
18780        let sub = make_resource(
18781            "AWS::SNS::Subscription",
18782            "MySub",
18783            serde_json::json!({
18784                "TopicArn": topic_arn,
18785                "Protocol": "sqs",
18786                "Endpoint": "arn:aws:sqs:us-east-1:123456789012:my-queue"
18787            }),
18788        );
18789        let result = prov.create_resource(&sub);
18790        assert!(result.is_ok());
18791    }
18792
18793    #[test]
18794    fn eventbridge_rule_arn_default_bus_omits_bus_name() {
18795        let prov = make_provisioner();
18796        let resource = make_resource(
18797            "AWS::Events::Rule",
18798            "MyRule",
18799            serde_json::json!({
18800                "Name": "my-rule",
18801                "ScheduleExpression": "rate(1 hour)"
18802            }),
18803        );
18804        let result = prov.create_resource(&resource).unwrap();
18805        // For default bus, ARN should be rule/<name> without /default/
18806        assert_eq!(
18807            result.physical_id,
18808            "arn:aws:events:us-east-1:123456789012:rule/my-rule"
18809        );
18810        assert!(!result.physical_id.contains("rule/default/"));
18811    }
18812
18813    #[test]
18814    fn eventbridge_rule_arn_custom_bus_includes_bus_name() {
18815        let prov = make_provisioner();
18816        // Create a custom bus first
18817        {
18818            let mut eb_accounts = prov.eventbridge_state.write();
18819            let state = eb_accounts.default_mut();
18820            state.buses.insert(
18821                "custom-bus".to_string(),
18822                fakecloud_eventbridge::EventBus {
18823                    name: "custom-bus".to_string(),
18824                    arn: "arn:aws:events:us-east-1:123456789012:event-bus/custom-bus".to_string(),
18825                    policy: None,
18826                    creation_time: Utc::now(),
18827                    last_modified_time: Utc::now(),
18828                    description: None,
18829                    kms_key_identifier: None,
18830                    dead_letter_config: None,
18831                    tags: std::collections::BTreeMap::new(),
18832                },
18833            );
18834        }
18835        let resource = make_resource(
18836            "AWS::Events::Rule",
18837            "MyRule",
18838            serde_json::json!({
18839                "Name": "my-rule",
18840                "EventBusName": "custom-bus",
18841                "ScheduleExpression": "rate(1 hour)"
18842            }),
18843        );
18844        let result = prov.create_resource(&resource).unwrap();
18845        assert_eq!(
18846            result.physical_id,
18847            "arn:aws:events:us-east-1:123456789012:rule/custom-bus/my-rule"
18848        );
18849    }
18850
18851    #[test]
18852    fn eventbridge_rule_rejects_nonexistent_bus() {
18853        let prov = make_provisioner();
18854        let resource = make_resource(
18855            "AWS::Events::Rule",
18856            "MyRule",
18857            serde_json::json!({
18858                "Name": "my-rule",
18859                "EventBusName": "nonexistent-bus",
18860                "ScheduleExpression": "rate(1 hour)"
18861            }),
18862        );
18863        let result = prov.create_resource(&resource);
18864        assert!(result.is_err());
18865        assert!(result.unwrap_err().contains("does not exist"));
18866    }
18867
18868    #[test]
18869    fn custom_resource_requires_service_token() {
18870        let prov = make_provisioner();
18871        let resource = make_resource(
18872            "Custom::MyResource",
18873            "MyCustom",
18874            serde_json::json!({
18875                "Foo": "bar"
18876            }),
18877        );
18878        let result = prov.create_resource(&resource);
18879        assert!(result.is_err());
18880        assert!(
18881            result.unwrap_err().contains("ServiceToken"),
18882            "Should require ServiceToken property"
18883        );
18884    }
18885
18886    #[test]
18887    fn custom_resource_succeeds_without_lambda_delivery() {
18888        // When no Lambda delivery is configured, custom resource creation
18889        // should still succeed (the invocation is silently skipped).
18890        let prov = make_provisioner();
18891        let resource = make_resource(
18892            "Custom::MyResource",
18893            "MyCustom",
18894            serde_json::json!({
18895                "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
18896                "Foo": "bar"
18897            }),
18898        );
18899        let result = prov.create_resource(&resource);
18900        assert!(result.is_ok());
18901        let sr = result.unwrap();
18902        assert_eq!(sr.logical_id, "MyCustom");
18903        assert_eq!(sr.resource_type, "Custom::MyResource");
18904        assert!(sr.physical_id.starts_with("MyCustom-"));
18905    }
18906
18907    #[test]
18908    fn cloudformation_custom_resource_type_succeeds() {
18909        let prov = make_provisioner();
18910        let resource = make_resource(
18911            "AWS::CloudFormation::CustomResource",
18912            "MyCustom2",
18913            serde_json::json!({
18914                "ServiceToken": "arn:aws:lambda:us-east-1:123456789012:function:my-func",
18915                "Key": "value"
18916            }),
18917        );
18918        let result = prov.create_resource(&resource);
18919        assert!(result.is_ok());
18920        let sr = result.unwrap();
18921        assert_eq!(sr.resource_type, "AWS::CloudFormation::CustomResource");
18922    }
18923
18924    // ── Resource create/delete lifecycle tests ──
18925
18926    #[test]
18927    fn sqs_queue_create_and_delete() {
18928        let prov = make_provisioner();
18929        let res = make_resource(
18930            "AWS::SQS::Queue",
18931            "MyQ",
18932            serde_json::json!({"QueueName": "my-q"}),
18933        );
18934        let sr = prov.create_resource(&res).unwrap();
18935        assert!(sr.physical_id.contains("my-q"));
18936        assert_eq!(sr.resource_type, "AWS::SQS::Queue");
18937        prov.delete_resource(&sr).unwrap();
18938    }
18939
18940    #[test]
18941    fn sqs_queue_fifo_with_suffix() {
18942        let prov = make_provisioner();
18943        let res = make_resource(
18944            "AWS::SQS::Queue",
18945            "FifoQ",
18946            serde_json::json!({"QueueName": "my-fifo.fifo", "FifoQueue": true}),
18947        );
18948        let sr = prov.create_resource(&res).unwrap();
18949        assert!(sr.physical_id.contains(".fifo"));
18950    }
18951
18952    #[test]
18953    fn sns_topic_create_and_delete() {
18954        let prov = make_provisioner();
18955        let res = make_resource(
18956            "AWS::SNS::Topic",
18957            "MyTopic",
18958            serde_json::json!({"TopicName": "t1"}),
18959        );
18960        let sr = prov.create_resource(&res).unwrap();
18961        assert!(sr.physical_id.contains("t1"));
18962        prov.delete_resource(&sr).unwrap();
18963    }
18964
18965    #[test]
18966    fn ssm_parameter_create_and_delete() {
18967        let prov = make_provisioner();
18968        let res = make_resource(
18969            "AWS::SSM::Parameter",
18970            "MyParam",
18971            serde_json::json!({
18972                "Name": "/my/param",
18973                "Type": "String",
18974                "Value": "v1"
18975            }),
18976        );
18977        let sr = prov.create_resource(&res).unwrap();
18978        assert_eq!(sr.physical_id, "/my/param");
18979        prov.delete_resource(&sr).unwrap();
18980    }
18981
18982    #[test]
18983    fn iam_role_create_and_delete() {
18984        let prov = make_provisioner();
18985        let res = make_resource(
18986            "AWS::IAM::Role",
18987            "MyRole",
18988            serde_json::json!({
18989                "RoleName": "my-role",
18990                "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
18991            }),
18992        );
18993        let sr = prov.create_resource(&res).unwrap();
18994        assert!(sr.physical_id.contains("my-role"));
18995        prov.delete_resource(&sr).unwrap();
18996    }
18997
18998    #[test]
18999    fn iam_policy_create_and_delete() {
19000        let prov = make_provisioner();
19001        let res = make_resource(
19002            "AWS::IAM::Policy",
19003            "MyPolicy",
19004            serde_json::json!({
19005                "PolicyName": "my-policy",
19006                "PolicyDocument": {"Version": "2012-10-17", "Statement": []}
19007            }),
19008        );
19009        let sr = prov.create_resource(&res).unwrap();
19010        assert!(sr.physical_id.contains("my-policy"));
19011        prov.delete_resource(&sr).unwrap();
19012    }
19013
19014    #[test]
19015    fn s3_bucket_create_and_delete() {
19016        let prov = make_provisioner();
19017        let res = make_resource(
19018            "AWS::S3::Bucket",
19019            "MyBucket",
19020            serde_json::json!({"BucketName": "my-bucket"}),
19021        );
19022        let sr = prov.create_resource(&res).unwrap();
19023        assert_eq!(sr.physical_id, "my-bucket");
19024        prov.delete_resource(&sr).unwrap();
19025    }
19026
19027    #[test]
19028    fn dynamodb_table_create_and_delete() {
19029        let prov = make_provisioner();
19030        let res = make_resource(
19031            "AWS::DynamoDB::Table",
19032            "MyTable",
19033            serde_json::json!({
19034                "TableName": "my-table",
19035                "KeySchema": [{"AttributeName": "pk", "KeyType": "HASH"}],
19036                "AttributeDefinitions": [{"AttributeName": "pk", "AttributeType": "S"}],
19037                "BillingMode": "PAY_PER_REQUEST"
19038            }),
19039        );
19040        let sr = prov.create_resource(&res).unwrap();
19041        assert!(sr.physical_id.contains("my-table"));
19042        prov.delete_resource(&sr).unwrap();
19043    }
19044
19045    #[test]
19046    fn log_group_create_and_delete() {
19047        let prov = make_provisioner();
19048        let res = make_resource(
19049            "AWS::Logs::LogGroup",
19050            "MyLogs",
19051            serde_json::json!({"LogGroupName": "/app/logs"}),
19052        );
19053        let sr = prov.create_resource(&res).unwrap();
19054        assert!(sr.physical_id.contains("/app/logs"));
19055        prov.delete_resource(&sr).unwrap();
19056    }
19057
19058    #[test]
19059    fn lambda_function_create_and_delete() {
19060        let prov = make_provisioner();
19061        let res = make_resource(
19062            "AWS::Lambda::Function",
19063            "MyFn",
19064            serde_json::json!({
19065                "FunctionName": "my-fn",
19066                "Runtime": "nodejs20.x",
19067                "Role": "arn:aws:iam::123456789012:role/lambda-role",
19068                "Handler": "index.handler",
19069                "MemorySize": 256,
19070                "Timeout": 10,
19071                "Environment": {"Variables": {"FOO": "bar"}}
19072            }),
19073        );
19074        let sr = prov.create_resource(&res).unwrap();
19075        assert_eq!(sr.physical_id, "my-fn");
19076        assert_eq!(
19077            sr.attributes.get("Arn").map(String::as_str),
19078            Some("arn:aws:lambda:us-east-1:123456789012:function:my-fn")
19079        );
19080        // Verify it landed in lambda state
19081        {
19082            let lam = prov.lambda_state.read();
19083            let st = lam.get("123456789012").unwrap();
19084            let f = st.functions.get("my-fn").unwrap();
19085            assert_eq!(f.runtime, "nodejs20.x");
19086            assert_eq!(f.memory_size, 256);
19087            assert_eq!(f.environment.get("FOO").unwrap(), "bar");
19088        }
19089        prov.delete_resource(&sr).unwrap();
19090        let lam = prov.lambda_state.read();
19091        let st = lam.get("123456789012").unwrap();
19092        assert!(!st.functions.contains_key("my-fn"));
19093    }
19094
19095    #[test]
19096    fn unsupported_resource_type_fails() {
19097        let prov = make_provisioner();
19098        let res = make_resource("AWS::NonExistent::Thing", "X", serde_json::json!({}));
19099        assert!(prov.create_resource(&res).is_err());
19100    }
19101
19102    #[test]
19103    fn iam_role_with_inline_policies() {
19104        let prov = make_provisioner();
19105        let res = make_resource(
19106            "AWS::IAM::Role",
19107            "MyRole",
19108            serde_json::json!({
19109                "RoleName": "role-inline",
19110                "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []},
19111                "Policies": [
19112                    {
19113                        "PolicyName": "inline-1",
19114                        "PolicyDocument": {"Version": "2012-10-17", "Statement": []}
19115                    }
19116                ]
19117            }),
19118        );
19119        let sr = prov.create_resource(&res).unwrap();
19120        assert!(sr.physical_id.contains("role-inline"));
19121    }
19122
19123    #[test]
19124    fn sqs_queue_auto_name() {
19125        let prov = make_provisioner();
19126        let res = make_resource("AWS::SQS::Queue", "AutoQ", serde_json::json!({}));
19127        let sr = prov.create_resource(&res).unwrap();
19128        // Generated queue name should exist
19129        assert!(!sr.physical_id.is_empty());
19130    }
19131
19132    #[test]
19133    fn sns_topic_auto_name() {
19134        let prov = make_provisioner();
19135        let res = make_resource("AWS::SNS::Topic", "AutoT", serde_json::json!({}));
19136        let sr = prov.create_resource(&res).unwrap();
19137        assert!(!sr.physical_id.is_empty());
19138    }
19139
19140    // ── additional resource types ──
19141
19142    #[test]
19143    fn unsupported_resource_type_errors() {
19144        let prov = make_provisioner();
19145        let res = make_resource("AWS::FooBar::Thing", "X", serde_json::json!({}));
19146        assert!(prov.create_resource(&res).is_err());
19147    }
19148
19149    #[test]
19150    fn sqs_queue_with_redrive_policy() {
19151        let prov = make_provisioner();
19152        // Create DLQ first
19153        let dlq = make_resource(
19154            "AWS::SQS::Queue",
19155            "DLQ",
19156            serde_json::json!({"QueueName": "dlq1"}),
19157        );
19158        let dlq_resource = prov.create_resource(&dlq).unwrap();
19159        let _ = dlq_resource.physical_id;
19160
19161        // Create source queue with redrive policy
19162        let src = make_resource(
19163            "AWS::SQS::Queue",
19164            "Src",
19165            serde_json::json!({
19166                "QueueName": "src1",
19167                "RedrivePolicy": {
19168                    "deadLetterTargetArn": "arn:aws:sqs:us-east-1:123456789012:dlq1",
19169                    "maxReceiveCount": 3
19170                }
19171            }),
19172        );
19173        let sr = prov.create_resource(&src).unwrap();
19174        assert!(!sr.physical_id.is_empty());
19175    }
19176
19177    #[test]
19178    fn sns_topic_with_display_name() {
19179        let prov = make_provisioner();
19180        let res = make_resource(
19181            "AWS::SNS::Topic",
19182            "WithName",
19183            serde_json::json!({"TopicName": "named-topic", "DisplayName": "Named"}),
19184        );
19185        let sr = prov.create_resource(&res).unwrap();
19186        assert!(sr.physical_id.contains("named-topic"));
19187    }
19188
19189    #[test]
19190    fn ssm_parameter_with_explicit_name() {
19191        let prov = make_provisioner();
19192        let res = make_resource(
19193            "AWS::SSM::Parameter",
19194            "Param",
19195            serde_json::json!({"Name": "/my/param", "Value": "v", "Type": "String"}),
19196        );
19197        let sr = prov.create_resource(&res).unwrap();
19198        assert!(sr.physical_id.contains("/my/param"));
19199    }
19200
19201    #[test]
19202    fn ssm_parameter_missing_name_errors() {
19203        let prov = make_provisioner();
19204        let res = make_resource(
19205            "AWS::SSM::Parameter",
19206            "AutoP",
19207            serde_json::json!({"Value": "v", "Type": "String"}),
19208        );
19209        assert!(prov.create_resource(&res).is_err());
19210    }
19211
19212    #[test]
19213    fn iam_managed_policy_auto_name() {
19214        let prov = make_provisioner();
19215        let res = make_resource(
19216            "AWS::IAM::Policy",
19217            "AutoPol",
19218            serde_json::json!({
19219                "PolicyName": "inline-pol",
19220                "PolicyDocument": {"Version": "2012-10-17", "Statement": []},
19221                "Users": []
19222            }),
19223        );
19224        let sr = prov.create_resource(&res).unwrap();
19225        assert!(!sr.physical_id.is_empty());
19226    }
19227
19228    #[test]
19229    fn delete_resource_works_for_queue() {
19230        let prov = make_provisioner();
19231        let res = make_resource(
19232            "AWS::SQS::Queue",
19233            "ToDel",
19234            serde_json::json!({"QueueName": "todel"}),
19235        );
19236        let sr = prov.create_resource(&res).unwrap();
19237        assert!(prov.delete_resource(&sr).is_ok());
19238    }
19239
19240    #[test]
19241    fn delete_resource_works_for_topic() {
19242        let prov = make_provisioner();
19243        let res = make_resource(
19244            "AWS::SNS::Topic",
19245            "DelT",
19246            serde_json::json!({"TopicName": "delt"}),
19247        );
19248        let sr = prov.create_resource(&res).unwrap();
19249        assert!(prov.delete_resource(&sr).is_ok());
19250    }
19251
19252    #[test]
19253    fn application_autoscaling_scalable_target_round_trip() {
19254        let prov = make_provisioner();
19255        let res = make_resource(
19256            "AWS::ApplicationAutoScaling::ScalableTarget",
19257            "Target",
19258            serde_json::json!({
19259                "ServiceNamespace": "ecs",
19260                "ResourceId": "service/my-cluster/my-service",
19261                "ScalableDimension": "ecs:service:DesiredCount",
19262                "MinCapacity": 1,
19263                "MaxCapacity": 10,
19264                "RoleARN": "arn:aws:iam::123456789012:role/my-role",
19265            }),
19266        );
19267        let sr = prov.create_resource(&res).unwrap();
19268        assert_eq!(sr.physical_id, "service/my-cluster/my-service");
19269        assert!(sr.attributes.contains_key("ScalableTargetARN"));
19270        assert!(prov.delete_resource(&sr).is_ok());
19271    }
19272
19273    #[test]
19274    fn application_autoscaling_scaling_policy_requires_target() {
19275        let prov = make_provisioner();
19276        let res = make_resource(
19277            "AWS::ApplicationAutoScaling::ScalingPolicy",
19278            "Policy",
19279            serde_json::json!({
19280                "PolicyName": "my-policy",
19281                "ServiceNamespace": "ecs",
19282                "ResourceId": "service/my-cluster/my-service",
19283                "ScalableDimension": "ecs:service:DesiredCount",
19284                "PolicyType": "TargetTrackingScaling",
19285                "TargetTrackingScalingPolicyConfiguration": {
19286                    "TargetValue": 50.0,
19287                    "PredefinedMetricSpecification": {
19288                        "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
19289                    }
19290                },
19291            }),
19292        );
19293        // Should fail because scalable target does not exist
19294        assert!(prov.create_resource(&res).is_err());
19295    }
19296
19297    #[test]
19298    fn application_autoscaling_scaling_policy_round_trip() {
19299        let prov = make_provisioner();
19300        let target = make_resource(
19301            "AWS::ApplicationAutoScaling::ScalableTarget",
19302            "Target",
19303            serde_json::json!({
19304                "ServiceNamespace": "ecs",
19305                "ResourceId": "service/my-cluster/my-service",
19306                "ScalableDimension": "ecs:service:DesiredCount",
19307                "MinCapacity": 1,
19308                "MaxCapacity": 10,
19309            }),
19310        );
19311        let sr = prov.create_resource(&target).unwrap();
19312
19313        let policy = make_resource(
19314            "AWS::ApplicationAutoScaling::ScalingPolicy",
19315            "Policy",
19316            serde_json::json!({
19317                "PolicyName": "my-policy",
19318                "ServiceNamespace": "ecs",
19319                "ResourceId": "service/my-cluster/my-service",
19320                "ScalableDimension": "ecs:service:DesiredCount",
19321                "PolicyType": "TargetTrackingScaling",
19322                "TargetTrackingScalingPolicyConfiguration": {
19323                    "TargetValue": 50.0,
19324                    "PredefinedMetricSpecification": {
19325                        "PredefinedMetricType": "ECSServiceAverageCPUUtilization"
19326                    }
19327                },
19328            }),
19329        );
19330        let psr = prov.create_resource(&policy).unwrap();
19331        assert!(psr.physical_id.starts_with("arn:aws:autoscaling:"));
19332        assert!(prov.delete_resource(&psr).is_ok());
19333        assert!(prov.delete_resource(&sr).is_ok());
19334    }
19335
19336    #[test]
19337    fn sqs_queue_with_fifo_suffix() {
19338        let prov = make_provisioner();
19339        let res = make_resource(
19340            "AWS::SQS::Queue",
19341            "Fifo",
19342            serde_json::json!({"QueueName": "fq.fifo", "FifoQueue": true}),
19343        );
19344        let sr = prov.create_resource(&res).unwrap();
19345        assert!(sr.physical_id.ends_with(".fifo"));
19346    }
19347
19348    // ── get_att dispatch ──
19349
19350    #[test]
19351    fn getatt_s3_bucket_arn_returns_arn() {
19352        let prov = make_provisioner();
19353        let bucket = make_resource(
19354            "AWS::S3::Bucket",
19355            "MyBucket",
19356            serde_json::json!({"BucketName": "my-bucket"}),
19357        );
19358        let sr = prov.create_resource(&bucket).unwrap();
19359        assert_eq!(
19360            prov.get_att(&sr, "Arn"),
19361            Some("arn:aws:s3:::my-bucket".to_string())
19362        );
19363    }
19364
19365    #[test]
19366    fn getatt_s3_bucket_domain_name_returns_dns_name() {
19367        let prov = make_provisioner();
19368        let bucket = make_resource(
19369            "AWS::S3::Bucket",
19370            "MyBucket",
19371            serde_json::json!({"BucketName": "my-bucket"}),
19372        );
19373        let sr = prov.create_resource(&bucket).unwrap();
19374        assert_eq!(
19375            prov.get_att(&sr, "DomainName"),
19376            Some("my-bucket.s3.amazonaws.com".to_string())
19377        );
19378    }
19379
19380    #[test]
19381    fn getatt_lambda_function_arn_returns_function_arn() {
19382        let prov = make_provisioner();
19383        // Lambda needs an existing IAM role to validate the function role.
19384        let role = make_resource(
19385            "AWS::IAM::Role",
19386            "MyRole",
19387            serde_json::json!({
19388                "RoleName": "my-role",
19389                "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
19390            }),
19391        );
19392        let role_sr = prov.create_resource(&role).unwrap();
19393        let fn_res = make_resource(
19394            "AWS::Lambda::Function",
19395            "MyFn",
19396            serde_json::json!({
19397                "FunctionName": "my-fn",
19398                "Runtime": "python3.11",
19399                "Handler": "index.handler",
19400                "Role": role_sr.physical_id,
19401                "Code": {"ZipFile": "def handler(e,c): return e"}
19402            }),
19403        );
19404        let fn_sr = prov.create_resource(&fn_res).unwrap();
19405        let arn = prov.get_att(&fn_sr, "Arn").expect("Arn should resolve");
19406        assert!(arn.starts_with("arn:aws:lambda:"));
19407        assert!(arn.contains(":function:my-fn"));
19408    }
19409
19410    #[test]
19411    fn getatt_iam_role_arn_returns_role_arn() {
19412        let prov = make_provisioner();
19413        let role = make_resource(
19414            "AWS::IAM::Role",
19415            "MyRole",
19416            serde_json::json!({
19417                "RoleName": "my-role",
19418                "AssumeRolePolicyDocument": {"Version": "2012-10-17", "Statement": []}
19419            }),
19420        );
19421        let sr = prov.create_resource(&role).unwrap();
19422        assert_eq!(
19423            prov.get_att(&sr, "Arn"),
19424            Some("arn:aws:iam::123456789012:role/my-role".to_string())
19425        );
19426        // RoleId is a real value generated at create time (FKIA-prefixed).
19427        let role_id = prov.get_att(&sr, "RoleId").expect("RoleId should resolve");
19428        assert!(role_id.starts_with("FKIA"));
19429    }
19430
19431    #[test]
19432    fn getatt_unknown_attribute_returns_none() {
19433        let prov = make_provisioner();
19434        let bucket = make_resource(
19435            "AWS::S3::Bucket",
19436            "MyBucket",
19437            serde_json::json!({"BucketName": "my-bucket"}),
19438        );
19439        let sr = prov.create_resource(&bucket).unwrap();
19440        // Fall-back behaviour: unknown attribute on a known type returns
19441        // None; the resolver in template.rs surfaces a "Logical.Attr"
19442        // placeholder so the existing template still builds.
19443        assert_eq!(prov.get_att(&sr, "NotARealAttr"), None);
19444    }
19445
19446    #[test]
19447    fn getatt_unknown_resource_type_returns_none() {
19448        let prov = make_provisioner();
19449        // Hand-crafted StackResource with a resource_type we don't dispatch
19450        // on at all. The captured attributes map is also empty, so there's
19451        // nothing to return.
19452        let stack_resource = StackResource {
19453            logical_id: "Mystery".to_string(),
19454            physical_id: "mystery-id".to_string(),
19455            resource_type: "AWS::Made::Up".to_string(),
19456            status: "CREATE_COMPLETE".to_string(),
19457            service_token: None,
19458            attributes: BTreeMap::new(),
19459        };
19460        assert_eq!(prov.get_att(&stack_resource, "Arn"), None);
19461    }
19462
19463    #[test]
19464    fn getatt_falls_back_to_captured_attributes() {
19465        let prov = make_provisioner();
19466        // Captured attributes always win — even if live-state dispatch
19467        // would return something different. This keeps `Fn::GetAtt`
19468        // deterministic for resources with cached attrs.
19469        let stack_resource = StackResource {
19470            logical_id: "MyTopic".to_string(),
19471            physical_id: "arn:aws:sns:us-east-1:123456789012:my-topic".to_string(),
19472            resource_type: "AWS::SNS::Topic".to_string(),
19473            status: "CREATE_COMPLETE".to_string(),
19474            service_token: None,
19475            attributes: {
19476                let mut m = BTreeMap::new();
19477                m.insert("TopicArn".to_string(), "captured-arn".to_string());
19478                m
19479            },
19480        };
19481        assert_eq!(
19482            prov.get_att(&stack_resource, "TopicArn"),
19483            Some("captured-arn".to_string())
19484        );
19485    }
19486
19487    #[test]
19488    fn getatt_secrets_manager_arn_resolves_via_live_state() {
19489        // Secrets create handler captures Id but not Arn; live-state
19490        // fallback fills in Arn.
19491        let prov = make_provisioner();
19492        let res = make_resource(
19493            "AWS::SecretsManager::Secret",
19494            "MySecret",
19495            serde_json::json!({"Name": "my-secret", "SecretString": "hunter2"}),
19496        );
19497        let sr = prov.create_resource(&res).unwrap();
19498        let arn = prov.get_att(&sr, "Arn").expect("Arn should resolve");
19499        assert!(arn.starts_with("arn:aws:secretsmanager:"));
19500        assert!(arn.ends_with(":secret:my-secret"));
19501    }
19502
19503    #[test]
19504    fn wafv2_web_acl_lifecycle() {
19505        let prov = make_provisioner();
19506        let res = make_resource(
19507            "AWS::WAFv2::WebACL",
19508            "MyAcl",
19509            serde_json::json!({
19510                "Name": "my-acl",
19511                "Scope": "REGIONAL",
19512                "DefaultAction": {"Allow": {}},
19513                "Rules": [{"Name": "rule1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
19514                "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "my-acl-metric"},
19515                "Capacity": 100,
19516            }),
19517        );
19518        let sr = prov.create_resource(&res).unwrap();
19519        assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19520        assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19521        assert_eq!(prov.get_att(&sr, "Name"), Some("my-acl".to_string()));
19522        assert!(prov.get_att(&sr, "Id").is_some());
19523        assert_eq!(prov.get_att(&sr, "Capacity"), Some("100".to_string()));
19524
19525        prov.delete_resource(&sr.clone()).unwrap();
19526        // Verify live-state fallback returns None by using a fresh resource
19527        // with empty attributes (captured attrs would still win).
19528        let fresh = StackResource {
19529            logical_id: "MyAcl".to_string(),
19530            physical_id: sr.physical_id.clone(),
19531            resource_type: "AWS::WAFv2::WebACL".to_string(),
19532            status: "CREATE_COMPLETE".to_string(),
19533            service_token: None,
19534            attributes: BTreeMap::new(),
19535        };
19536        assert_eq!(prov.get_att(&fresh, "Arn"), None);
19537    }
19538
19539    #[test]
19540    fn wafv2_ip_set_lifecycle() {
19541        let prov = make_provisioner();
19542        let res = make_resource(
19543            "AWS::WAFv2::IPSet",
19544            "MyIpSet",
19545            serde_json::json!({
19546                "Name": "my-ipset",
19547                "Scope": "REGIONAL",
19548                "IPAddressVersion": "IPV4",
19549                "Addresses": ["10.0.0.0/8"],
19550            }),
19551        );
19552        let sr = prov.create_resource(&res).unwrap();
19553        assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19554        assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19555        assert_eq!(prov.get_att(&sr, "Name"), Some("my-ipset".to_string()));
19556
19557        prov.delete_resource(&sr.clone()).unwrap();
19558        let fresh = StackResource {
19559            logical_id: "MyIpSet".to_string(),
19560            physical_id: sr.physical_id.clone(),
19561            resource_type: "AWS::WAFv2::IPSet".to_string(),
19562            status: "CREATE_COMPLETE".to_string(),
19563            service_token: None,
19564            attributes: BTreeMap::new(),
19565        };
19566        assert_eq!(prov.get_att(&fresh, "Arn"), None);
19567    }
19568
19569    #[test]
19570    fn wafv2_regex_pattern_set_lifecycle() {
19571        let prov = make_provisioner();
19572        let res = make_resource(
19573            "AWS::WAFv2::RegexPatternSet",
19574            "MyRegexSet",
19575            serde_json::json!({
19576                "Name": "my-regex",
19577                "Scope": "REGIONAL",
19578                "RegularExpressions": [{"RegexString": "^test"}],
19579            }),
19580        );
19581        let sr = prov.create_resource(&res).unwrap();
19582        assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19583        assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19584        assert_eq!(prov.get_att(&sr, "Name"), Some("my-regex".to_string()));
19585
19586        prov.delete_resource(&sr.clone()).unwrap();
19587        let fresh = StackResource {
19588            logical_id: "MyRegexSet".to_string(),
19589            physical_id: sr.physical_id.clone(),
19590            resource_type: "AWS::WAFv2::RegexPatternSet".to_string(),
19591            status: "CREATE_COMPLETE".to_string(),
19592            service_token: None,
19593            attributes: BTreeMap::new(),
19594        };
19595        assert_eq!(prov.get_att(&fresh, "Arn"), None);
19596    }
19597
19598    #[test]
19599    fn wafv2_rule_group_lifecycle() {
19600        let prov = make_provisioner();
19601        let res = make_resource(
19602            "AWS::WAFv2::RuleGroup",
19603            "MyRuleGroup",
19604            serde_json::json!({
19605                "Name": "my-rg",
19606                "Scope": "REGIONAL",
19607                "Capacity": 50,
19608                "Rules": [{"Name": "r1", "Priority": 1, "Statement": {}, "VisibilityConfig": {}}],
19609                "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "rg-metric"},
19610            }),
19611        );
19612        let sr = prov.create_resource(&res).unwrap();
19613        assert!(sr.physical_id.starts_with("arn:aws:wafv2:"));
19614        assert_eq!(prov.get_att(&sr, "Arn"), Some(sr.physical_id.clone()));
19615        assert_eq!(prov.get_att(&sr, "Name"), Some("my-rg".to_string()));
19616
19617        prov.delete_resource(&sr.clone()).unwrap();
19618        let fresh = StackResource {
19619            logical_id: "MyRuleGroup".to_string(),
19620            physical_id: sr.physical_id.clone(),
19621            resource_type: "AWS::WAFv2::RuleGroup".to_string(),
19622            status: "CREATE_COMPLETE".to_string(),
19623            service_token: None,
19624            attributes: BTreeMap::new(),
19625        };
19626        assert_eq!(prov.get_att(&fresh, "Arn"), None);
19627    }
19628
19629    #[test]
19630    fn wafv2_logging_configuration_lifecycle() {
19631        let prov = make_provisioner();
19632        let res = make_resource(
19633            "AWS::WAFv2::LoggingConfiguration",
19634            "MyLogConfig",
19635            serde_json::json!({
19636                "ResourceArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc",
19637                "LogDestinationConfigs": ["arn:aws:logs:us-east-1:123456789012:log-group:/aws/waf"],
19638            }),
19639        );
19640        let sr = prov.create_resource(&res).unwrap();
19641        assert_eq!(
19642            sr.physical_id,
19643            "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/test/abc"
19644        );
19645
19646        prov.delete_resource(&sr.clone()).unwrap();
19647    }
19648
19649    #[test]
19650    fn wafv2_web_acl_association_lifecycle() {
19651        let prov = make_provisioner();
19652        let res = make_resource(
19653            "AWS::WAFv2::WebACLAssociation",
19654            "MyAssoc",
19655            serde_json::json!({
19656                "ResourceArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188",
19657                "WebACLArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-acl/abc",
19658            }),
19659        );
19660        let sr = prov.create_resource(&res).unwrap();
19661        assert_eq!(sr.physical_id, "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/50dc6c495c0c9188");
19662
19663        prov.delete_resource(&sr.clone()).unwrap();
19664    }
19665
19666    #[test]
19667    fn ses_configuration_set_lifecycle() {
19668        let prov = make_provisioner();
19669        let res = make_resource(
19670            "AWS::SES::ConfigurationSet",
19671            "MyConfigSet",
19672            serde_json::json!({
19673                "Name": "my-cs",
19674                "SendingOptions": {"SendingEnabled": true},
19675                "DeliveryOptions": {"TlsPolicy": "REQUIRE"},
19676            }),
19677        );
19678        let sr = prov.create_resource(&res).unwrap();
19679        assert_eq!(sr.physical_id, "my-cs");
19680        assert_eq!(prov.get_att(&sr, "Name"), Some("my-cs".to_string()));
19681
19682        prov.delete_resource(&sr.clone()).unwrap();
19683        let fresh = StackResource {
19684            logical_id: "MyConfigSet".to_string(),
19685            physical_id: "my-cs".to_string(),
19686            resource_type: "AWS::SES::ConfigurationSet".to_string(),
19687            status: "CREATE_COMPLETE".to_string(),
19688            service_token: None,
19689            attributes: BTreeMap::new(),
19690        };
19691        assert_eq!(prov.get_att(&fresh, "Name"), None);
19692    }
19693
19694    #[test]
19695    fn ses_email_identity_lifecycle() {
19696        let prov = make_provisioner();
19697        let res = make_resource(
19698            "AWS::SES::EmailIdentity",
19699            "MyIdentity",
19700            serde_json::json!({"EmailIdentity": "example.com"}),
19701        );
19702        let sr = prov.create_resource(&res).unwrap();
19703        assert_eq!(sr.physical_id, "example.com");
19704        assert_eq!(
19705            prov.get_att(&sr, "IdentityName"),
19706            Some("example.com".to_string())
19707        );
19708
19709        prov.delete_resource(&sr.clone()).unwrap();
19710        let fresh = StackResource {
19711            logical_id: "MyIdentity".to_string(),
19712            physical_id: "example.com".to_string(),
19713            resource_type: "AWS::SES::EmailIdentity".to_string(),
19714            status: "CREATE_COMPLETE".to_string(),
19715            service_token: None,
19716            attributes: BTreeMap::new(),
19717        };
19718        assert_eq!(prov.get_att(&fresh, "IdentityName"), None);
19719    }
19720
19721    #[test]
19722    fn ses_template_lifecycle() {
19723        let prov = make_provisioner();
19724        let res = make_resource(
19725            "AWS::SES::Template",
19726            "MyTemplate",
19727            serde_json::json!({
19728                "Template": {
19729                    "TemplateName": "my-tpl",
19730                    "SubjectPart": "Hello",
19731                    "HtmlPart": "<h1>Hi</h1>",
19732                    "TextPart": "Hi",
19733                },
19734            }),
19735        );
19736        let sr = prov.create_resource(&res).unwrap();
19737        assert_eq!(sr.physical_id, "my-tpl");
19738        assert_eq!(
19739            prov.get_att(&sr, "TemplateName"),
19740            Some("my-tpl".to_string())
19741        );
19742
19743        prov.delete_resource(&sr.clone()).unwrap();
19744        let fresh = StackResource {
19745            logical_id: "MyTemplate".to_string(),
19746            physical_id: "my-tpl".to_string(),
19747            resource_type: "AWS::SES::Template".to_string(),
19748            status: "CREATE_COMPLETE".to_string(),
19749            service_token: None,
19750            attributes: BTreeMap::new(),
19751        };
19752        assert_eq!(prov.get_att(&fresh, "TemplateName"), None);
19753    }
19754
19755    #[test]
19756    fn ses_contact_list_lifecycle() {
19757        let prov = make_provisioner();
19758        let res = make_resource(
19759            "AWS::SES::ContactList",
19760            "MyContactList",
19761            serde_json::json!({
19762                "ContactListName": "my-cl",
19763                "Description": "Test contacts",
19764                "Topics": [{"TopicName": "news", "DisplayName": "Newsletter", "Description": "Weekly news"}],
19765            }),
19766        );
19767        let sr = prov.create_resource(&res).unwrap();
19768        assert_eq!(sr.physical_id, "my-cl");
19769        assert_eq!(
19770            prov.get_att(&sr, "ContactListName"),
19771            Some("my-cl".to_string())
19772        );
19773
19774        prov.delete_resource(&sr.clone()).unwrap();
19775        let fresh = StackResource {
19776            logical_id: "MyContactList".to_string(),
19777            physical_id: "my-cl".to_string(),
19778            resource_type: "AWS::SES::ContactList".to_string(),
19779            status: "CREATE_COMPLETE".to_string(),
19780            service_token: None,
19781            attributes: BTreeMap::new(),
19782        };
19783        assert_eq!(prov.get_att(&fresh, "ContactListName"), None);
19784    }
19785
19786    #[test]
19787    fn ses_dedicated_ip_pool_lifecycle() {
19788        let prov = make_provisioner();
19789        let res = make_resource(
19790            "AWS::SES::DedicatedIpPool",
19791            "MyPool",
19792            serde_json::json!({"PoolName": "my-pool", "ScalingMode": "STANDARD"}),
19793        );
19794        let sr = prov.create_resource(&res).unwrap();
19795        assert_eq!(sr.physical_id, "my-pool");
19796        assert_eq!(prov.get_att(&sr, "PoolName"), Some("my-pool".to_string()));
19797
19798        prov.delete_resource(&sr.clone()).unwrap();
19799        let fresh = StackResource {
19800            logical_id: "MyPool".to_string(),
19801            physical_id: "my-pool".to_string(),
19802            resource_type: "AWS::SES::DedicatedIpPool".to_string(),
19803            status: "CREATE_COMPLETE".to_string(),
19804            service_token: None,
19805            attributes: BTreeMap::new(),
19806        };
19807        assert_eq!(prov.get_att(&fresh, "PoolName"), None);
19808    }
19809
19810    #[test]
19811    fn ses_receipt_rule_set_lifecycle() {
19812        let prov = make_provisioner();
19813        let res = make_resource(
19814            "AWS::SES::ReceiptRuleSet",
19815            "MyRuleSet",
19816            serde_json::json!({"RuleSetName": "my-rs"}),
19817        );
19818        let sr = prov.create_resource(&res).unwrap();
19819        assert_eq!(sr.physical_id, "my-rs");
19820        assert_eq!(prov.get_att(&sr, "RuleSetName"), Some("my-rs".to_string()));
19821
19822        prov.delete_resource(&sr.clone()).unwrap();
19823        let fresh = StackResource {
19824            logical_id: "MyRuleSet".to_string(),
19825            physical_id: "my-rs".to_string(),
19826            resource_type: "AWS::SES::ReceiptRuleSet".to_string(),
19827            status: "CREATE_COMPLETE".to_string(),
19828            service_token: None,
19829            attributes: BTreeMap::new(),
19830        };
19831        assert_eq!(prov.get_att(&fresh, "RuleSetName"), None);
19832    }
19833
19834    #[test]
19835    fn ses_receipt_rule_lifecycle() {
19836        let prov = make_provisioner();
19837        let rs = make_resource(
19838            "AWS::SES::ReceiptRuleSet",
19839            "MyRuleSet",
19840            serde_json::json!({"RuleSetName": "my-rs2"}),
19841        );
19842        prov.create_resource(&rs).unwrap();
19843
19844        let res = make_resource(
19845            "AWS::SES::ReceiptRule",
19846            "MyRule",
19847            serde_json::json!({
19848                "RuleSetName": "my-rs2",
19849                "Rule": {
19850                    "Name": "rule1",
19851                    "Priority": 1,
19852                    "Enabled": true,
19853                    "Actions": [{"S3Action": {"BucketName": "my-bucket"}}],
19854                },
19855            }),
19856        );
19857        let sr = prov.create_resource(&res).unwrap();
19858        assert_eq!(sr.physical_id, "my-rs2|rule1");
19859
19860        prov.delete_resource(&sr.clone()).unwrap();
19861    }
19862
19863    #[test]
19864    fn ses_receipt_filter_lifecycle() {
19865        let prov = make_provisioner();
19866        let res = make_resource(
19867            "AWS::SES::ReceiptFilter",
19868            "MyFilter",
19869            serde_json::json!({
19870                "Filter": {
19871                    "Name": "my-filter",
19872                    "IpFilter": {"Policy": "Block", "Cidr": "10.0.0.0/8"},
19873                },
19874            }),
19875        );
19876        let sr = prov.create_resource(&res).unwrap();
19877        assert_eq!(sr.physical_id, "my-filter");
19878
19879        prov.delete_resource(&sr.clone()).unwrap();
19880    }
19881
19882    #[test]
19883    fn ses_vdm_attributes_lifecycle() {
19884        let prov = make_provisioner();
19885        let res = make_resource(
19886            "AWS::SES::VdmAttributes",
19887            "MyVdm",
19888            serde_json::json!({
19889                "DashboardAttributes": {"EngagementMetrics": "ENABLED"},
19890                "GuardianAttributes": {"OptimizedSharedDelivery": "ENABLED"},
19891            }),
19892        );
19893        let sr = prov.create_resource(&res).unwrap();
19894        assert_eq!(sr.physical_id, "vdm-MyVdm");
19895
19896        prov.delete_resource(&sr.clone()).unwrap();
19897    }
19898
19899    #[test]
19900    fn athena_work_group_lifecycle() {
19901        let prov = make_provisioner();
19902        let res = make_resource(
19903            "AWS::Athena::WorkGroup",
19904            "MyWg",
19905            serde_json::json!({
19906                "Name": "my-wg",
19907                "Description": "test wg",
19908                "Configuration": {"EnforceWorkGroupConfiguration": true},
19909            }),
19910        );
19911        let sr = prov.create_resource(&res).unwrap();
19912        assert_eq!(sr.physical_id, "my-wg");
19913        assert_eq!(sr.attributes.get("Name"), Some(&"my-wg".to_string()));
19914        assert!(sr
19915            .attributes
19916            .get("Arn")
19917            .unwrap()
19918            .contains("workgroup/my-wg"));
19919
19920        assert_eq!(
19921            prov.get_att(
19922                &StackResource {
19923                    resource_type: "AWS::Athena::WorkGroup".to_string(),
19924                    physical_id: sr.physical_id.clone(),
19925                    logical_id: "MyWg".to_string(),
19926                    status: "CREATE_COMPLETE".to_string(),
19927                    service_token: None,
19928                    attributes: BTreeMap::new(),
19929                },
19930                "Name",
19931            ),
19932            Some("my-wg".to_string()),
19933        );
19934
19935        prov.delete_resource(&sr.clone()).unwrap();
19936    }
19937
19938    #[test]
19939    fn athena_data_catalog_lifecycle() {
19940        let prov = make_provisioner();
19941        let res = make_resource(
19942            "AWS::Athena::DataCatalog",
19943            "MyCatalog",
19944            serde_json::json!({
19945                "Name": "my-catalog",
19946                "Type": "GLUE",
19947                "Description": "test catalog",
19948            }),
19949        );
19950        let sr = prov.create_resource(&res).unwrap();
19951        assert_eq!(sr.physical_id, "my-catalog");
19952        assert_eq!(sr.attributes.get("Name"), Some(&"my-catalog".to_string()));
19953        assert!(sr
19954            .attributes
19955            .get("Arn")
19956            .unwrap()
19957            .contains("datacatalog/my-catalog"));
19958
19959        prov.delete_resource(&sr.clone()).unwrap();
19960    }
19961
19962    #[test]
19963    fn athena_named_query_lifecycle() {
19964        let prov = make_provisioner();
19965        let res = make_resource(
19966            "AWS::Athena::NamedQuery",
19967            "MyQuery",
19968            serde_json::json!({
19969                "Name": "my-query",
19970                "Database": "mydb",
19971                "QueryString": "SELECT 1",
19972                "WorkGroup": "primary",
19973            }),
19974        );
19975        let sr = prov.create_resource(&res).unwrap();
19976        assert!(!sr.physical_id.is_empty());
19977        assert_eq!(sr.attributes.get("NamedQueryId"), Some(&sr.physical_id));
19978
19979        prov.delete_resource(&sr.clone()).unwrap();
19980    }
19981
19982    #[test]
19983    fn athena_prepared_statement_lifecycle() {
19984        let prov = make_provisioner();
19985        let res = make_resource(
19986            "AWS::Athena::PreparedStatement",
19987            "MyPs",
19988            serde_json::json!({
19989                "StatementName": "my-ps",
19990                "WorkGroupName": "primary",
19991                "QueryStatement": "SELECT 1",
19992            }),
19993        );
19994        let sr = prov.create_resource(&res).unwrap();
19995        assert_eq!(sr.physical_id, "primary|my-ps");
19996
19997        prov.delete_resource(&sr.clone()).unwrap();
19998    }
19999}