Skip to main content

winterbaume_ec2/
state.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::types::*;
4
5#[derive(Debug, Default)]
6pub struct Ec2State {
7    pub vpcs: HashMap<String, Vpc>,
8    pub subnets: HashMap<String, Subnet>,
9    pub igws: HashMap<String, InternetGateway>,
10    pub security_groups: HashMap<String, SecurityGroup>,
11    pub route_tables: HashMap<String, RouteTable>,
12    pub key_pairs: HashMap<String, KeyPair>,
13    pub network_acls: HashMap<String, NetworkAcl>,
14    pub elastic_ips: HashMap<String, ElasticIp>,
15    pub nat_gateways: HashMap<String, NatGateway>,
16    pub dhcp_options: HashMap<String, DhcpOptions>,
17    pub egress_only_igws: HashMap<String, EgressOnlyInternetGateway>,
18    pub flow_logs: HashMap<String, FlowLog>,
19    pub vpc_peering_connections: HashMap<String, VpcPeeringConnection>,
20    pub vpc_endpoints: HashMap<String, VpcEndpoint>,
21    pub managed_prefix_lists: HashMap<String, ManagedPrefixList>,
22    pub customer_gateways: HashMap<String, CustomerGateway>,
23    pub vpn_gateways: HashMap<String, VpnGateway>,
24    pub vpn_connections: HashMap<String, VpnConnection>,
25    pub carrier_gateways: HashMap<String, CarrierGateway>,
26    pub network_interfaces: HashMap<String, NetworkInterface>,
27    pub vpc_cidr_associations: HashMap<String, String>, // assoc_id -> cidr_block
28    pub ebs_encryption_by_default: bool,
29    /// Account-level EBS default KMS key ID, set by `ModifyEbsDefaultKmsKeyId`
30    /// and reset by `ResetEbsDefaultKmsKeyId`. `None` means use the AWS-managed
31    /// default `alias/aws/ebs`.
32    pub ebs_default_kms_key_id: Option<String>,
33    /// Toggle for `EnableSerialConsoleAccess` / `DisableSerialConsoleAccess`.
34    pub serial_console_access_enabled: bool,
35    /// State for `EnableAllowedImagesSettings` / `DisableAllowedImagesSettings`,
36    /// independent of the criteria list. `None` means never configured (treated
37    /// as `disabled`).
38    pub allowed_images_settings_state: Option<String>,
39    /// Account-level state for `EnableImageBlockPublicAccess`. One of
40    /// `block-new-sharing`, `unblocked`. `None` means never configured.
41    pub image_block_public_access_state: Option<String>,
42    /// Account-level AWS Network Performance metric subscriptions, keyed by
43    /// `(source, destination, metric, statistic)`.
44    pub aws_network_performance_subscriptions:
45        HashMap<(String, String, String, String), AwsNetworkPerformanceSubscription>,
46    pub transit_gateways: HashMap<String, TransitGateway>,
47    pub tgw_vpc_attachments: HashMap<String, TransitGatewayVpcAttachment>,
48    pub tgw_peering_attachments: HashMap<String, TransitGatewayPeeringAttachment>,
49    pub tgw_route_tables: HashMap<String, TransitGatewayRouteTable>,
50    pub tgw_routes: HashMap<String, Vec<TransitGatewayRoute>>,
51    pub instances: HashMap<String, Instance>,
52    pub volumes: HashMap<String, Volume>,
53    pub snapshots: HashMap<String, Snapshot>,
54    pub images: HashMap<String, Image>,
55    /// IDs of images that were explicitly deregistered. Used to suppress
56    /// stub-synthesis in `describe_images` for IDs that once existed.
57    pub deregistered_images: HashSet<String>,
58    pub launch_templates: HashMap<String, LaunchTemplate>,
59    pub launch_template_versions: HashMap<String, Vec<LaunchTemplateVersion>>,
60    pub spot_requests: HashMap<String, SpotInstanceRequest>,
61    /// Singleton per-account spot-instance datafeed subscription. `None`
62    /// after `DeleteSpotDatafeedSubscription` or before any
63    /// `CreateSpotDatafeedSubscription`; `Describe` should return an
64    /// `InvalidSpotDatafeed.NotFound` error when this is `None`.
65    pub spot_datafeed_subscription: Option<SpotDatafeedSubscription>,
66    pub iam_instance_profile_associations: HashMap<String, IamInstanceProfileAssociation>,
67    pub dedicated_hosts: HashMap<String, DedicatedHost>,
68    pub ec2_fleets: HashMap<String, Ec2Fleet>,
69    pub vpc_endpoint_service_configs: HashMap<String, VpcEndpointServiceConfiguration>,
70    pub spot_fleet_requests: HashMap<String, SpotFleetRequest>,
71    pub subnet_cidr_reservations: HashMap<String, SubnetCidrReservationEntry>,
72    pub placement_groups: HashMap<String, PlacementGroup>,
73    pub network_interface_permissions: HashMap<String, NetworkInterfacePermission>,
74    pub instance_connect_endpoints: HashMap<String, InstanceConnectEndpoint>,
75    pub capacity_reservations: HashMap<String, CapacityReservation>,
76    pub capacity_reservation_fleets: HashMap<String, CapacityReservationFleet>,
77    pub coip_pools: HashMap<String, CoipPool>,
78    /// Bring-your-own-IP CIDRs, keyed by CIDR.
79    pub byoip_cidrs: HashMap<String, ByoipCidr>,
80    /// Public IPv4 pools, keyed by pool ID.
81    pub public_ipv4_pools: HashMap<String, PublicIpv4Pool>,
82    /// Customer-owned IP CIDRs, keyed by `(cidr, coip_pool_id)`.
83    pub coip_cidrs: HashMap<(String, String), CoipCidr>,
84    /// Pending / accepted EIP transfer offers, keyed by allocation ID.
85    pub address_transfers: HashMap<String, AddressTransfer>,
86    /// Keyed by `(group_id, vpc_id)`.
87    pub security_group_vpc_associations: HashMap<(String, String), SecurityGroupVpcAssociation>,
88    /// Keyed by `(certificate_arn, role_arn)`.
89    pub enclave_certificate_iam_role_associations:
90        HashMap<(String, String), EnclaveCertificateIamRoleAssociation>,
91    pub mac_sip_modification_tasks: HashMap<String, MacSipModificationTask>,
92    pub declarative_policies_reports: HashMap<String, DeclarativePoliciesReport>,
93    /// Mock VPN concentrator records, keyed by `vpn-concentrator-...` ID.
94    pub vpn_concentrators: HashMap<String, VpnConcentrator>,
95    /// Pending / accepted endpoint connections to a service configuration,
96    /// keyed by `(service_id, vpc_endpoint_id)`.
97    pub vpc_endpoint_connections: HashMap<(String, String), VpcEndpointConnection>,
98    /// VPC endpoint connection notifications, keyed by notification ID.
99    pub vpc_endpoint_connection_notifications: HashMap<String, VpcEndpointConnectionNotification>,
100    /// VPC block public access exclusions, keyed by exclusion ID.
101    pub vpc_block_public_access_exclusions: HashMap<String, VpcBlockPublicAccessExclusion>,
102    /// Account/region-level block public access options. There is at most one
103    /// of these per account/region; this is the live value (or `None` when
104    /// `ModifyVpcBlockPublicAccessOptions` has never been called).
105    pub vpc_block_public_access_options: Option<VpcBlockPublicAccessOptions>,
106    /// VPC encryption controls, keyed by `vpc-encryption-control-...` ID.
107    pub vpc_encryption_controls: HashMap<String, VpcEncryptionControl>,
108    // --- Group 4 additions ---
109    /// Mac volume ownership delegation tasks, keyed by task ID.
110    pub mac_volume_ownership_tasks: HashMap<String, MacVolumeOwnershipTask>,
111    /// Replace-root-volume tasks, keyed by task ID.
112    pub replace_root_volume_tasks: HashMap<String, ReplaceRootVolumeTask>,
113    /// Snapshot import tasks, keyed by `import-snap-...` ID.
114    pub snapshot_import_tasks: HashMap<String, SnapshotImportTask>,
115    /// Conversion tasks (ImportInstance / ImportVolume), keyed by task ID.
116    pub conversion_tasks: HashMap<String, ConversionTask>,
117    /// Export tasks (CreateInstanceExportTask), keyed by export task ID.
118    pub export_tasks: HashMap<String, ExportTask>,
119    /// Generic import tasks (e.g. ImportImage), keyed by import task ID.
120    /// Stores `(state, previous_state)` so `CancelImportTask` can report
121    /// the transition.
122    pub import_tasks: HashMap<String, (String, Option<String>)>,
123    /// Branch / trunk interface associations, keyed by association ID.
124    pub trunk_interface_associations: HashMap<String, TrunkInterfaceAssociation>,
125    /// Secondary networks, keyed by network ID.
126    pub secondary_networks: HashMap<String, SecondaryNetwork>,
127    /// Secondary subnets, keyed by subnet ID.
128    pub secondary_subnets: HashMap<String, SecondarySubnet>,
129    /// Volumes that have been logically deleted but are recoverable via
130    /// `RestoreVolumeFromRecycleBin`. Mock policy treats every volume
131    /// deletion as recoverable.
132    pub deleted_volumes_recycle_bin: HashMap<String, Volume>,
133    /// Snapshots that have been logically deleted but are recoverable via
134    /// `RestoreSnapshotFromRecycleBin`.
135    pub deleted_snapshots_recycle_bin: HashMap<String, Snapshot>,
136    // --- Group 5 additions ---
137    /// Reserved Instance exchange-quote acceptance records, keyed by exchange ID.
138    pub reserved_instances_exchanges: HashMap<String, ReservedInstancesExchange>,
139    /// Reserved Instance marketplace listings, keyed by listing ID.
140    pub reserved_instances_listings: HashMap<String, ReservedInstancesListing>,
141    /// Queued (not-yet-active) Reserved Instance purchase records.
142    pub queued_reserved_instances_purchases: HashMap<String, ReservedInstancesPurchase>,
143    /// Reserved Instance modification requests, keyed by modification ID.
144    pub reserved_instances_modifications: HashMap<String, ReservedInstancesModification>,
145    /// Active Reserved Instance purchases, keyed by purchase ID.
146    pub reserved_instances_purchases: HashMap<String, ReservedInstancesPurchase>,
147    /// Active Reserved Instances, keyed by ReservedInstancesId.
148    pub reserved_instances: HashMap<String, ReservedInstances>,
149    /// FPGA images, keyed by FpgaImageId.
150    pub fpga_images: HashMap<String, FpgaImage>,
151    /// Image-usage reports, keyed by ReportId.
152    pub image_usage_reports: HashMap<String, ImageUsageReport>,
153    /// In-progress and completed `CreateRestoreImageTask` records, keyed by image_id.
154    pub restore_image_tasks: HashMap<String, RestoreImageTask>,
155    /// In-progress and completed `CreateStoreImageTask` records, keyed by ami_id.
156    pub store_image_tasks: HashMap<String, StoreImageTask>,
157    /// `ImportImage` task records, keyed by import-task-id.
158    pub import_image_tasks: HashMap<String, ImportImageTask>,
159    /// Per-region allowed AMI criteria.
160    pub allowed_image_criteria: Vec<AllowedImageCriterion>,
161    /// Per-account default credit specification, keyed by instance family.
162    pub default_credit_specifications: HashMap<String, String>,
163    /// Account-level / per-region instance metadata defaults.
164    pub instance_metadata_defaults: Option<InstanceMetadataDefaults>,
165    /// Instance event windows, keyed by InstanceEventWindowId.
166    pub instance_event_windows: HashMap<String, InstanceEventWindow>,
167    /// Instance event notification attributes (single-record-per-account).
168    pub instance_event_notification_attributes: Option<InstanceTagNotificationAttributes>,
169    /// Scheduled instance status events, keyed by event_id.
170    pub instance_events: HashMap<String, InstanceEvent>,
171    /// Host reservations, keyed by host-reservation ID.
172    pub host_reservations: HashMap<String, HostReservation>,
173    /// Scheduled instances, keyed by scheduled_instance_id.
174    pub scheduled_instances: HashMap<String, ScheduledInstance>,
175    /// Per-AZ-group opt-in status.
176    pub az_group_opt_in: HashMap<String, String>,
177    /// User-submitted instance status reports.
178    pub instance_status_reports: Vec<InstanceStatusReport>,
179    // --- Group 6 additions ---
180    /// Network Insights access scopes, keyed by scope ID.
181    pub network_insights_access_scopes: HashMap<String, NetworkInsightsAccessScope>,
182    /// Network Insights access scope analyses, keyed by analysis ID.
183    pub network_insights_access_scope_analyses: HashMap<String, NetworkInsightsAccessScopeAnalysis>,
184    /// Network Insights paths, keyed by path ID.
185    pub network_insights_paths: HashMap<String, NetworkInsightsPath>,
186    /// Network Insights analyses, keyed by analysis ID.
187    pub network_insights_analyses: HashMap<String, NetworkInsightsAnalysis>,
188    /// Traffic Mirror filters, keyed by filter ID.
189    pub traffic_mirror_filters: HashMap<String, TrafficMirrorFilter>,
190    /// Traffic Mirror sessions, keyed by session ID.
191    pub traffic_mirror_sessions: HashMap<String, TrafficMirrorSession>,
192    /// Traffic Mirror targets, keyed by target ID.
193    pub traffic_mirror_targets: HashMap<String, TrafficMirrorTarget>,
194    // --- Group 7 additions ---
195    /// Client VPN endpoints, keyed by endpoint ID.
196    pub client_vpn_endpoints: HashMap<String, ClientVpnEndpoint>,
197    /// Client VPN target-network associations, keyed by association ID.
198    pub client_vpn_target_network_associations: HashMap<String, ClientVpnTargetNetworkAssociation>,
199    /// Client VPN authorization rules, keyed by `(endpoint_id, destination_cidr, group_id)`.
200    pub client_vpn_authorization_rules:
201        HashMap<(String, String, String), ClientVpnAuthorizationRule>,
202    /// Client VPN routes, keyed by `(endpoint_id, destination_cidr, target_subnet)`.
203    pub client_vpn_routes: HashMap<(String, String, String), ClientVpnRoute>,
204    /// Client VPN connections, keyed by connection ID.
205    pub client_vpn_connections: HashMap<String, ClientVpnConnection>,
206    /// Local Gateways, keyed by `lgw-...` ID. Seeded on demand by tests/handlers.
207    pub local_gateways: HashMap<String, LocalGateway>,
208    /// Local Gateway route tables, keyed by `lgw-rtb-...` ID.
209    pub local_gateway_route_tables: HashMap<String, LocalGatewayRouteTable>,
210    /// Local Gateway routes, keyed by `(local_gateway_route_table_id, destination_cidr_block)`.
211    pub local_gateway_routes: HashMap<(String, String), LocalGatewayRoute>,
212    /// Virtual-interface group associations, keyed by association ID.
213    pub local_gateway_route_table_virtual_interface_group_associations:
214        HashMap<String, LocalGatewayRouteTableVirtualInterfaceGroupAssociation>,
215    /// VPC associations, keyed by association ID.
216    pub local_gateway_route_table_vpc_associations:
217        HashMap<String, LocalGatewayRouteTableVpcAssociation>,
218    /// Local Gateway virtual interfaces, keyed by virtual interface ID.
219    pub local_gateway_virtual_interfaces: HashMap<String, LocalGatewayVirtualInterface>,
220    /// Local Gateway virtual interface groups, keyed by group ID.
221    pub local_gateway_virtual_interface_groups: HashMap<String, LocalGatewayVirtualInterfaceGroup>,
222    // --- Group 8 additions ---
223    /// Route Servers, keyed by `rs-...` ID.
224    pub route_servers: HashMap<String, RouteServer>,
225    /// Route Server endpoints, keyed by endpoint ID.
226    pub route_server_endpoints: HashMap<String, RouteServerEndpoint>,
227    /// Route Server peers, keyed by peer ID.
228    pub route_server_peers: HashMap<String, RouteServerPeer>,
229    /// Route Server <-> VPC associations, keyed by `(route_server_id, vpc_id)`.
230    pub route_server_associations: HashMap<(String, String), RouteServerAssociation>,
231    // --- Group 9 additions ---
232    /// Verified Access instances, keyed by `vai-...` ID.
233    pub verified_access_instances: HashMap<String, VerifiedAccessInstance>,
234    /// Verified Access trust providers, keyed by `vatp-...` ID.
235    pub verified_access_trust_providers: HashMap<String, VerifiedAccessTrustProvider>,
236    /// Verified Access groups, keyed by `vagr-...` ID.
237    pub verified_access_groups: HashMap<String, VerifiedAccessGroup>,
238    /// Verified Access endpoints, keyed by `vae-...` ID.
239    pub verified_access_endpoints: HashMap<String, VerifiedAccessEndpoint>,
240    /// Trust-provider <-> instance attachments, keyed by `(instance_id, trust_provider_id)`.
241    pub verified_access_trust_provider_attachments:
242        HashMap<(String, String), VerifiedAccessTrustProviderAttachment>,
243    /// Per-instance logging configuration, keyed by instance ID.
244    pub verified_access_instance_logging_configurations: HashMap<String, VerifiedAccessLogs>,
245    // --- Group 10 additions ---
246    /// Pending / accepted billing-ownership offers, keyed by `(capacity_reservation_id, account_id)`.
247    pub billing_ownership_offers: HashMap<(String, String), BillingOwnershipOffer>,
248    /// Capacity Manager data exports, keyed by export ID.
249    pub capacity_manager_data_exports: HashMap<String, CapacityManagerDataExport>,
250    /// Interruptible capacity-reservation allocations, keyed by allocation ID.
251    pub interruptible_capacity_reservation_allocations:
252        HashMap<String, InterruptibleCapacityReservationAllocation>,
253    /// Capacity Block purchases, keyed by capacity block ID.
254    pub capacity_blocks: HashMap<String, CapacityBlock>,
255    /// Capacity Block extensions, keyed by extension ID.
256    pub capacity_block_extensions: HashMap<String, CapacityBlockExtension>,
257    /// Account-level Capacity Manager Organizations access state.
258    pub capacity_manager_organizations_access: Option<CapacityManagerOrganizationsAccess>,
259    // --- Group 11 additions: Transit Gateway extension families ---
260    /// TGW multicast domains, keyed by `tgw-mcast-domain-...` ID.
261    pub tgw_multicast_domains: HashMap<String, TransitGatewayMulticastDomain>,
262    /// TGW multicast domain↔VPC associations, keyed by `(domain_id, attachment_id)`.
263    pub tgw_multicast_domain_associations:
264        HashMap<(String, String), TransitGatewayMulticastDomainAssociation>,
265    /// TGW multicast group members, keyed by `(domain_id, group_ip, network_interface_id)`.
266    pub tgw_multicast_group_members:
267        HashMap<(String, String, String), TransitGatewayMulticastGroupMember>,
268    /// TGW multicast group sources, keyed by `(domain_id, group_ip, network_interface_id)`.
269    pub tgw_multicast_group_sources:
270        HashMap<(String, String, String), TransitGatewayMulticastGroupSource>,
271    /// TGW connect attachments, keyed by attachment ID.
272    pub tgw_connects: HashMap<String, TransitGatewayConnect>,
273    /// TGW connect peers, keyed by `tgw-connect-peer-...` ID.
274    pub tgw_connect_peers: HashMap<String, TransitGatewayConnectPeer>,
275    /// TGW metering policies, keyed by `tgw-mp-...` ID.
276    pub tgw_metering_policies: HashMap<String, TransitGatewayMeteringPolicy>,
277    /// TGW metering policy entries, keyed by `(policy_id, entry_id)`.
278    pub tgw_metering_policy_entries: HashMap<(String, String), TransitGatewayMeteringPolicyEntry>,
279    /// TGW policy tables, keyed by `tgw-rtb-policy-...` ID.
280    pub tgw_policy_tables: HashMap<String, TransitGatewayPolicyTable>,
281    /// TGW policy table associations, keyed by `(policy_table_id, attachment_id)`.
282    pub tgw_policy_table_associations:
283        HashMap<(String, String), TransitGatewayPolicyTableAssociation>,
284    /// TGW prefix-list references, keyed by `(route_table_id, prefix_list_id)`.
285    pub tgw_prefix_list_references: HashMap<(String, String), TransitGatewayPrefixListReference>,
286    /// TGW route-table announcements, keyed by announcement ID.
287    pub tgw_route_table_announcements: HashMap<String, TransitGatewayRouteTableAnnouncement>,
288    // --- Group 12 additions: IPAM (IP Address Manager) families ---
289    /// IPAMs, keyed by `ipam-...` ID.
290    pub ipams: HashMap<String, Ipam>,
291    /// IPAM scopes, keyed by `ipam-scope-...` ID.
292    pub ipam_scopes: HashMap<String, IpamScope>,
293    /// IPAM pools, keyed by `ipam-pool-...` ID.
294    pub ipam_pools: HashMap<String, IpamPool>,
295    /// IPAM pool CIDRs, keyed by `(pool_id, cidr)`.
296    pub ipam_pool_cidrs: HashMap<(String, String), IpamPoolCidr>,
297    /// IPAM pool allocations, keyed by `(pool_id, allocation_id)`.
298    pub ipam_pool_allocations: HashMap<(String, String), IpamPoolAllocation>,
299    /// IPAM resource discoveries, keyed by `ipam-res-disco-...` ID.
300    pub ipam_resource_discoveries: HashMap<String, IpamResourceDiscovery>,
301    /// IPAM resource discovery associations, keyed by association ID.
302    pub ipam_resource_discovery_associations: HashMap<String, IpamResourceDiscoveryAssociation>,
303    /// IPAM BYO-ASNs, keyed by `(ipam_id, asn)`.
304    pub ipam_byoasns: HashMap<(String, String), IpamByoasn>,
305    /// IPAM external resource verification tokens, keyed by token ID.
306    pub ipam_external_resource_verification_tokens:
307        HashMap<String, IpamExternalResourceVerificationToken>,
308    /// IPAM policies, keyed by `ipam-policy-...` ID.
309    pub ipam_policies: HashMap<String, IpamPolicy>,
310    /// IPAM prefix list resolvers, keyed by resolver ID.
311    pub ipam_prefix_list_resolvers: HashMap<String, IpamPrefixListResolver>,
312    /// IPAM prefix list resolver targets, keyed by `(resolver_id, target_id)`.
313    pub ipam_prefix_list_resolver_targets: HashMap<(String, String), IpamPrefixListResolverTarget>,
314    // --- Batch B additions ---
315    /// `ModifyVolume` records, keyed by volume_id.
316    pub volume_modifications: HashMap<String, VolumeModification>,
317    /// `ImportVolume` conversion tasks, keyed by conversion_task_id.
318    pub import_volume_tasks: HashMap<String, ImportVolumeTask>,
319    /// `BundleInstance` tasks, keyed by bundle_id.
320    pub bundle_tasks: HashMap<String, BundleTask>,
321    /// Per-resource-type long/short ID format toggles. Defaults to long IDs
322    /// for any resource not explicitly modified.
323    pub id_format: HashMap<String, IdFormatEntry>,
324    /// Outpost LAGs, keyed by outpost-lag ID.
325    pub outpost_lags: HashMap<String, OutpostLag>,
326    /// `ExportImage` tasks, keyed by export_image_task_id.
327    pub export_image_tasks: HashMap<String, ExportImageTask>,
328    pub counters: Ec2Counters,
329}
330
331#[derive(Debug, Default)]
332pub struct Ec2Counters {
333    pub vpc: u32,
334    pub subnet: u32,
335    pub igw: u32,
336    pub sg: u32,
337    pub sgr: u32,
338    pub rtb: u32,
339    pub rtbassoc: u32,
340    pub keypair: u32,
341    pub nacl: u32,
342    pub nacl_assoc: u32,
343    pub eip: u32,
344    pub eip_assoc: u32,
345    pub nat: u32,
346    pub dopt: u32,
347    pub eigw: u32,
348    pub flow_log: u32,
349    pub vpc_peering: u32,
350    pub vpc_endpoint: u32,
351    pub prefix_list: u32,
352    pub cgw: u32,
353    pub vgw: u32,
354    pub vpn: u32,
355    pub cgw_carrier: u32,
356    pub eni: u32,
357    pub eni_attach: u32,
358    pub vpc_cidr_assoc: u32,
359    pub tgw: u32,
360    pub tgw_attach: u32,
361    pub tgw_rtb: u32,
362    pub instance: u32,
363    pub vol: u32,
364    pub snapshot: u32,
365    pub ami: u32,
366    pub lt: u32,
367    pub spot: u32,
368    pub iam_assoc: u32,
369    pub host: u32,
370    pub fleet: u32,
371    pub vpce_svc: u32,
372    pub spot_fleet: u32,
373    pub subnet_cidr_res: u32,
374    pub subnet_ipv6_assoc: u32,
375    pub placement_group: u32,
376    pub eni_permission: u32,
377    pub instance_connect_endpoint: u32,
378    pub capacity_reservation: u32,
379    pub capacity_reservation_fleet: u32,
380    pub coip_pool: u32,
381    pub mac_sip_task: u32,
382    pub declarative_policies_report: u32,
383    pub public_ipv4_pool: u32,
384    pub address_transfer: u32,
385    pub nat_gateway_address_assoc: u32,
386    pub vpn_concentrator: u32,
387    pub vpc_endpoint_connection_notification: u32,
388    pub vpc_block_public_access_exclusion: u32,
389    pub vpc_encryption_control: u32,
390    // --- Group 4 additions ---
391    pub mac_volume_ownership_task: u32,
392    pub replace_root_volume_task: u32,
393    pub snapshot_import_task: u32,
394    pub conversion_task: u32,
395    pub export_task: u32,
396    pub import_task: u32,
397    pub trunk_interface_assoc: u32,
398    pub secondary_network: u32,
399    pub secondary_subnet: u32,
400    // --- Group 5 additions ---
401    pub reserved_instances_exchange: u32,
402    pub reserved_instances_listing: u32,
403    pub reserved_instances_purchase: u32,
404    pub reserved_instances: u32,
405    pub reserved_instances_modification: u32,
406    pub fpga_image: u32,
407    pub image_usage_report: u32,
408    pub import_image_task: u32,
409    pub instance_event_window: u32,
410    pub instance_event: u32,
411    pub host_reservation: u32,
412    pub scheduled_instance: u32,
413    // --- Group 6 additions ---
414    pub network_insights_access_scope: u32,
415    pub network_insights_access_scope_analysis: u32,
416    pub network_insights_path: u32,
417    pub network_insights_analysis: u32,
418    pub traffic_mirror_filter: u32,
419    pub traffic_mirror_filter_rule: u32,
420    pub traffic_mirror_session: u32,
421    pub traffic_mirror_target: u32,
422    // --- Group 7 additions ---
423    pub client_vpn_endpoint: u32,
424    pub client_vpn_target_network_association: u32,
425    pub client_vpn_connection: u32,
426    pub local_gateway: u32,
427    pub local_gateway_route_table: u32,
428    pub local_gateway_route_table_virtual_interface_group_association: u32,
429    pub local_gateway_route_table_vpc_association: u32,
430    pub local_gateway_virtual_interface: u32,
431    pub local_gateway_virtual_interface_group: u32,
432    // --- Group 8 additions ---
433    pub route_server: u32,
434    pub route_server_endpoint: u32,
435    pub route_server_peer: u32,
436    // --- Group 9 additions ---
437    pub verified_access_instance: u32,
438    pub verified_access_trust_provider: u32,
439    pub verified_access_group: u32,
440    pub verified_access_endpoint: u32,
441    // --- Group 10 additions ---
442    pub capacity_manager_data_export: u32,
443    pub interruptible_capacity_reservation_allocation: u32,
444    pub capacity_block: u32,
445    pub capacity_block_extension: u32,
446    // --- Group 11 additions ---
447    pub tgw_multicast_domain: u32,
448    pub tgw_connect: u32,
449    pub tgw_connect_peer: u32,
450    pub tgw_metering_policy: u32,
451    pub tgw_metering_policy_entry: u32,
452    pub tgw_policy_table: u32,
453    pub tgw_route_table_announcement: u32,
454    // --- Group 12 additions ---
455    pub ipam: u32,
456    pub ipam_scope: u32,
457    pub ipam_pool: u32,
458    pub ipam_pool_cidr: u32,
459    pub ipam_pool_allocation: u32,
460    pub ipam_resource_discovery: u32,
461    pub ipam_resource_discovery_association: u32,
462    pub ipam_external_resource_verification_token: u32,
463    pub ipam_policy: u32,
464    pub ipam_prefix_list_resolver: u32,
465    pub ipam_prefix_list_resolver_target: u32,
466    // --- Batch B additions ---
467    pub bundle_task: u32,
468    pub volume_modification: u32,
469    pub import_volume_task: u32,
470    pub export_image_task: u32,
471    pub outpost_lag: u32,
472}
473
474#[derive(Debug, thiserror::Error)]
475pub enum Ec2Error {
476    #[error("The vpc ID '{0}' does not exist")]
477    VpcNotFound(String),
478
479    #[error("The network ACL '{0}' does not exist")]
480    NetworkAclNotFound(String),
481
482    #[error("Cannot delete the default network ACL")]
483    CannotDeleteDefaultNetworkAcl,
484
485    #[error("DHCP options set is associated with a VPC")]
486    DhcpOptionsAssociatedWithVpc,
487
488    #[error("The association '{0}' does not exist")]
489    AssociationNotFound(String),
490
491    #[error("The association ID '{0}' does not exist")]
492    AssociationIdNotFound(String),
493
494    #[error("The allocation ID '{0}' does not exist")]
495    AllocationNotFound(String),
496
497    #[error("The subnet ID '{0}' does not exist")]
498    SubnetNotFound(String),
499
500    #[error("The NAT gateway '{0}' does not exist")]
501    NatGatewayNotFound(String),
502
503    #[error("The DHCP options ID '{0}' does not exist")]
504    DhcpOptionsNotFound(String),
505
506    #[error("The internetGateway ID '{0}' does not exist")]
507    InternetGatewayNotFound(String),
508
509    #[error("The security group '{0}' does not exist")]
510    SecurityGroupNotFound(String),
511
512    #[error("The routeTable ID '{0}' does not exist")]
513    RouteTableNotFound(String),
514
515    #[error("The TGW route table '{0}' does not exist")]
516    TgwRouteTableNotFound(String),
517
518    #[error("The route with destination '{0}' does not exist")]
519    RouteNotFound(String),
520
521    #[error("The egress-only internet gateway '{0}' does not exist")]
522    EgressOnlyIgwNotFound(String),
523
524    #[error("The VPC peering connection '{0}' does not exist")]
525    VpcPeeringConnectionNotFound(String),
526
527    #[error("The prefix list '{0}' does not exist")]
528    PrefixListNotFound(String),
529
530    #[error("The customer gateway '{0}' does not exist")]
531    CustomerGatewayNotFound(String),
532
533    #[error("The VPN gateway '{0}' does not exist")]
534    VpnGatewayNotFound(String),
535
536    #[error("The VPN connection '{0}' does not exist")]
537    VpnConnectionNotFound(String),
538
539    #[error("The carrier gateway '{0}' does not exist")]
540    CarrierGatewayNotFound(String),
541
542    #[error("The network interface '{0}' does not exist")]
543    NetworkInterfaceNotFound(String),
544
545    #[error("The attachment '{0}' does not exist")]
546    AttachmentNotFound(String),
547
548    #[error("The CIDR block association '{0}' does not exist")]
549    VpcCidrBlockAssociationNotFound(String),
550
551    #[error("The volume '{0}' does not exist")]
552    VolumeNotFound(String),
553
554    #[error("The instance '{0}' does not exist")]
555    InstanceNotFound(String),
556
557    #[error("Volume is not attached to the specified instance/device")]
558    VolumeNotAttached,
559
560    #[error("The snapshot '{0}' does not exist")]
561    SnapshotNotFound(String),
562
563    #[error("The AMI '{0}' does not exist")]
564    AmiNotFound(String),
565
566    #[error("A spot datafeed subscription already exists for this account")]
567    SpotDatafeedAlreadyExists,
568
569    #[error("The spot datafeed subscription for this account was not found")]
570    SpotDatafeedNotFound,
571
572    #[error("A launch template with name '{0}' already exists")]
573    LaunchTemplateAlreadyExists(String),
574
575    #[error("Launch template '{0}' does not exist")]
576    LaunchTemplateNotFound(String),
577
578    #[error("The transit gateway '{0}' does not exist")]
579    TransitGatewayNotFound(String),
580
581    #[error("The TGW attachment '{0}' does not exist")]
582    TgwVpcAttachmentNotFound(String),
583
584    #[error("The peering attachment '{0}' does not exist")]
585    TgwPeeringAttachmentNotFound(String),
586
587    #[error("The endpoint '{0}' does not exist")]
588    VpcEndpointNotFound(String),
589
590    #[error("The VPC endpoint service ID '{0}' does not exist")]
591    VpcEndpointServiceNotFound(String),
592
593    #[error("The CIDR reservation '{0}' does not exist")]
594    SubnetCidrReservationNotFound(String),
595
596    #[error("A Default VPC already exists for this user in this region.")]
597    DefaultVpcAlreadyExists,
598
599    #[error("No default VPC found")]
600    DefaultVpcNotFound,
601
602    #[error("The placement group '{0}' does not exist")]
603    PlacementGroupNotFound(String),
604
605    #[error("The placement group '{0}' already exists")]
606    PlacementGroupAlreadyExists(String),
607
608    #[error("{0}")]
609    InvalidParameterValue(String),
610
611    #[error("The network interface permission '{0}' does not exist")]
612    NetworkInterfacePermissionNotFound(String),
613
614    #[error("The instance connect endpoint '{0}' does not exist")]
615    InstanceConnectEndpointNotFound(String),
616
617    #[error("The capacity reservation '{0}' does not exist")]
618    CapacityReservationNotFound(String),
619
620    #[error("The capacity reservation fleet '{0}' does not exist")]
621    CapacityReservationFleetNotFound(String),
622
623    #[error("The COIP pool '{0}' does not exist")]
624    CoipPoolNotFound(String),
625
626    #[error("The security group VPC association for group '{0}' and VPC '{1}' does not exist")]
627    SecurityGroupVpcAssociationNotFound(String, String),
628
629    #[error(
630        "The enclave certificate IAM role association for certificate '{0}' and role '{1}' does not exist"
631    )]
632    EnclaveCertificateIamRoleAssociationNotFound(String, String),
633
634    #[error("The Mac SIP modification task '{0}' does not exist")]
635    MacSipModificationTaskNotFound(String),
636
637    #[error("The declarative policies report '{0}' does not exist")]
638    DeclarativePoliciesReportNotFound(String),
639
640    #[error("The declarative policies report '{0}' is not in a cancellable state")]
641    DeclarativePoliciesReportNotCancellable(String),
642
643    #[error("The BYOIP CIDR '{0}' does not exist")]
644    InvalidByoipCidrNotFound(String),
645
646    #[error("The BYOIP CIDR '{0}' already exists")]
647    ByoipCidrAlreadyExists(String),
648
649    #[error("The BYOIP CIDR '{0}' is not in a state that allows this operation")]
650    ByoipCidrInvalidState(String),
651
652    #[error("The public IPv4 pool '{0}' does not exist")]
653    InvalidPublicIpv4PoolNotFound(String),
654
655    #[error("The public IPv4 pool '{0}' is not empty")]
656    PublicIpv4PoolNotEmpty(String),
657
658    #[error("The CIDR '{0}' does not belong to the public IPv4 pool '{1}'")]
659    PublicIpv4PoolCidrNotFound(String, String),
660
661    #[error("The COIP CIDR '{0}' in pool '{1}' does not exist")]
662    CoipCidrNotFound(String, String),
663
664    #[error("The COIP CIDR '{0}' in pool '{1}' already exists")]
665    CoipCidrAlreadyExists(String, String),
666
667    #[error("The address transfer for allocation '{0}' does not exist")]
668    InvalidAddressTransferNotFound(String),
669
670    #[error(
671        "The NAT gateway secondary address (allocation '{0}' / private IP '{1}') does not exist"
672    )]
673    InvalidNatGatewaySecondaryAddressNotFound(String, String),
674
675    #[error("The VPN concentrator '{0}' does not exist")]
676    InvalidVpnConcentratorNotFound(String),
677
678    #[error("The VPC endpoint connection (service '{0}', endpoint '{1}') does not exist")]
679    InvalidVpcEndpointConnectionNotFound(String, String),
680
681    #[error("The VPC endpoint connection notification '{0}' does not exist")]
682    InvalidVpcEndpointConnectionNotificationNotFound(String),
683
684    #[error("The VPC block public access exclusion '{0}' does not exist")]
685    InvalidVpcBlockPublicAccessExclusionNotFound(String),
686
687    #[error("VPC block public access options have not been configured for this account/region")]
688    InvalidVpcBlockPublicAccessOptionsNotFound,
689
690    #[error("The VPC encryption control '{0}' does not exist")]
691    InvalidVpcEncryptionControlNotFound(String),
692
693    #[error(
694        "The VPN connection static route ({vpn_connection_id} -> {destination_cidr}) does not exist"
695    )]
696    InvalidVpnConnectionRouteNotFound {
697        vpn_connection_id: String,
698        destination_cidr: String,
699    },
700
701    #[error(
702        "The VPN tunnel ({vpn_connection_id} / {outside_ip_address}) does not exist on this VPN connection"
703    )]
704    InvalidVpnTunnelNotFound {
705        vpn_connection_id: String,
706        outside_ip_address: String,
707    },
708
709    #[error("The conversion task '{0}' does not exist")]
710    InvalidConversionTaskNotFound(String),
711
712    #[error("The export task '{0}' does not exist")]
713    InvalidExportTaskNotFound(String),
714
715    #[error("The import task '{0}' does not exist")]
716    InvalidImportTaskNotFound(String),
717
718    #[error("The snapshot import task '{0}' does not exist")]
719    InvalidSnapshotImportTaskNotFound(String),
720
721    #[error("The Mac volume ownership delegation task '{0}' does not exist")]
722    InvalidMacVolumeOwnershipTaskNotFound(String),
723
724    #[error("The replace-root-volume task '{0}' does not exist")]
725    InvalidReplaceRootVolumeTaskNotFound(String),
726
727    #[error("The secondary network '{0}' does not exist")]
728    InvalidSecondaryNetworkNotFound(String),
729
730    #[error("The secondary subnet '{0}' does not exist")]
731    InvalidSecondarySubnetNotFound(String),
732
733    #[error("The trunk interface association '{0}' does not exist")]
734    InvalidTrunkInterfaceAssociationNotFound(String),
735
736    #[error("The secondary network '{0}' has dependent secondary subnets")]
737    SecondaryNetworkHasSubnets(String),
738
739    #[error("The snapshot '{0}' is locked and cannot be modified or deleted")]
740    SnapshotIsLocked(String),
741
742    #[error("The snapshot '{0}' is not in the recycle bin")]
743    SnapshotNotInRecycleBin(String),
744
745    #[error("The volume '{0}' is not in the recycle bin")]
746    VolumeNotInRecycleBin(String),
747
748    #[error("The FPGA image '{0}' does not exist")]
749    InvalidFpgaImageNotFound(String),
750
751    #[error("The import image task '{0}' does not exist")]
752    InvalidImportImageTaskNotFound(String),
753
754    #[error("The store image task for AMI '{0}' does not exist")]
755    InvalidStoreImageTaskNotFound(String),
756
757    #[error("The restore image task for image '{0}' does not exist")]
758    InvalidRestoreImageTaskNotFound(String),
759
760    #[error("The image usage report '{0}' does not exist")]
761    InvalidImageUsageReportNotFound(String),
762
763    #[error("The instance event window '{0}' does not exist")]
764    InvalidInstanceEventWindowNotFound(String),
765
766    #[error("The instance event '{0}' does not exist")]
767    InvalidInstanceEventNotFound(String),
768
769    #[error("The host reservation '{0}' does not exist")]
770    InvalidHostReservationNotFound(String),
771
772    #[error("The scheduled instance '{0}' does not exist")]
773    InvalidScheduledInstanceNotFound(String),
774
775    #[error("The reserved instances listing '{0}' does not exist")]
776    InvalidReservedInstancesListingNotFound(String),
777
778    #[error("The reserved instances exchange '{0}' does not exist")]
779    InvalidReservedInstancesExchangeNotFound(String),
780
781    #[error("The reserved instances modification '{0}' does not exist")]
782    InvalidReservedInstancesModificationNotFound(String),
783
784    #[error("The queued reserved instances purchase '{0}' does not exist")]
785    InvalidQueuedReservedInstancesNotFound(String),
786
787    #[error("The launch template version '{0}' does not exist")]
788    InvalidLaunchTemplateVersionNotFound(i64),
789
790    #[error("The image '{0}' is not in the recycle bin")]
791    ImageNotInRecycleBin(String),
792
793    #[error("The network insights access scope '{0}' does not exist")]
794    InvalidNetworkInsightsAccessScopeNotFound(String),
795
796    #[error("The network insights access scope analysis '{0}' does not exist")]
797    InvalidNetworkInsightsAccessScopeAnalysisNotFound(String),
798
799    #[error("The network insights path '{0}' does not exist")]
800    InvalidNetworkInsightsPathNotFound(String),
801
802    #[error("The network insights analysis '{0}' does not exist")]
803    InvalidNetworkInsightsAnalysisNotFound(String),
804
805    #[error("The network insights path '{0}' has active analyses and cannot be deleted")]
806    NetworkInsightsPathHasAnalyses(String),
807
808    #[error("The traffic mirror filter '{0}' does not exist")]
809    InvalidTrafficMirrorFilterNotFound(String),
810
811    #[error("The traffic mirror filter '{0}' is in use by one or more sessions")]
812    TrafficMirrorFilterInUse(String),
813
814    #[error("The traffic mirror filter rule '{0}' does not exist")]
815    InvalidTrafficMirrorFilterRuleNotFound(String),
816
817    #[error("The traffic mirror session '{0}' does not exist")]
818    InvalidTrafficMirrorSessionNotFound(String),
819
820    #[error("The traffic mirror target '{0}' does not exist")]
821    InvalidTrafficMirrorTargetNotFound(String),
822
823    #[error("The traffic mirror target '{0}' is in use by one or more sessions")]
824    TrafficMirrorTargetInUse(String),
825
826    // --- Group 7 errors ---
827    #[error("The Client VPN endpoint '{0}' does not exist")]
828    InvalidClientVpnEndpointNotFound(String),
829
830    #[error("The Client VPN endpoint '{0}' is in use and cannot be deleted")]
831    ClientVpnEndpointInUse(String),
832
833    #[error("The Client VPN target-network association '{0}' does not exist")]
834    InvalidClientVpnTargetNetworkAssociationNotFound(String),
835
836    #[error("The Client VPN authorization rule for endpoint '{0}' / cidr '{1}' does not exist")]
837    InvalidClientVpnAuthorizationRuleNotFound(String, String),
838
839    #[error("The Client VPN route for endpoint '{0}' / cidr '{1}' / subnet '{2}' does not exist")]
840    InvalidClientVpnRouteNotFound(String, String, String),
841
842    #[error("The local gateway '{0}' does not exist")]
843    InvalidLocalGatewayNotFound(String),
844
845    #[error("The local gateway route table '{0}' does not exist")]
846    InvalidLocalGatewayRouteTableNotFound(String),
847
848    #[error("The local gateway route table '{0}' is in use and has dependent routes")]
849    LocalGatewayRouteTableInUse(String),
850
851    #[error("The local gateway route ({0}, {1}) does not exist")]
852    InvalidLocalGatewayRouteNotFound(String, String),
853
854    #[error("The local gateway virtual interface '{0}' does not exist")]
855    InvalidLocalGatewayVirtualInterfaceNotFound(String),
856
857    #[error("The local gateway virtual interface '{0}' is in use by a virtual interface group")]
858    LocalGatewayVirtualInterfaceInUse(String),
859
860    #[error("The local gateway virtual interface group '{0}' does not exist")]
861    InvalidLocalGatewayVirtualInterfaceGroupNotFound(String),
862
863    #[error(
864        "The local gateway virtual interface group '{0}' is in use and has dependent interfaces or associations"
865    )]
866    LocalGatewayVirtualInterfaceGroupInUse(String),
867
868    #[error(
869        "The local gateway route table virtual interface group association '{0}' does not exist"
870    )]
871    InvalidLocalGatewayRouteTableVirtualInterfaceGroupAssociationNotFound(String),
872
873    #[error("The local gateway route table VPC association '{0}' does not exist")]
874    InvalidLocalGatewayRouteTableVpcAssociationNotFound(String),
875
876    // --- Group 8 errors ---
877    #[error("The route server '{0}' does not exist")]
878    InvalidRouteServerNotFound(String),
879
880    #[error("The route server '{0}' is in use and cannot be deleted")]
881    RouteServerInUse(String),
882
883    #[error("The route server endpoint '{0}' does not exist")]
884    InvalidRouteServerEndpointNotFound(String),
885
886    #[error("The route server endpoint '{0}' is in use and cannot be deleted")]
887    RouteServerEndpointInUse(String),
888
889    #[error("The route server peer '{0}' does not exist")]
890    InvalidRouteServerPeerNotFound(String),
891
892    #[error("The route server association for route server '{0}' and VPC '{1}' does not exist")]
893    InvalidRouteServerAssociationNotFound(String, String),
894
895    // --- Group 9 errors ---
896    #[error("The Verified Access instance '{0}' does not exist")]
897    InvalidVerifiedAccessInstanceNotFound(String),
898
899    #[error("The Verified Access instance '{0}' is in use and cannot be deleted")]
900    VerifiedAccessInstanceInUse(String),
901
902    #[error("The Verified Access trust provider '{0}' does not exist")]
903    InvalidVerifiedAccessTrustProviderNotFound(String),
904
905    #[error("The Verified Access trust provider '{0}' is in use and cannot be deleted")]
906    VerifiedAccessTrustProviderInUse(String),
907
908    #[error("The Verified Access group '{0}' does not exist")]
909    InvalidVerifiedAccessGroupNotFound(String),
910
911    #[error("The Verified Access group '{0}' is in use and cannot be deleted")]
912    VerifiedAccessGroupInUse(String),
913
914    #[error("The Verified Access endpoint '{0}' does not exist")]
915    InvalidVerifiedAccessEndpointNotFound(String),
916
917    #[error(
918        "The Verified Access trust provider attachment (instance '{0}', trust provider '{1}') does not exist"
919    )]
920    InvalidVerifiedAccessTrustProviderAttachmentNotFound(String, String),
921
922    // --- Group 10 errors ---
923    #[error(
924        "The billing ownership offer for capacity reservation '{0}' and account '{1}' does not exist"
925    )]
926    InvalidBillingOwnershipOfferNotFound(String, String),
927
928    #[error(
929        "A billing ownership offer for capacity reservation '{0}' and account '{1}' already exists"
930    )]
931    BillingOwnershipOfferAlreadyExists(String, String),
932
933    #[error("The capacity manager data export '{0}' does not exist")]
934    InvalidCapacityManagerDataExportNotFound(String),
935
936    #[error("The interruptible capacity reservation allocation '{0}' does not exist")]
937    InvalidInterruptibleCapacityReservationAllocationNotFound(String),
938
939    #[error("The capacity block '{0}' does not exist")]
940    InvalidCapacityBlockNotFound(String),
941
942    #[error("The capacity block extension '{0}' does not exist")]
943    InvalidCapacityBlockExtensionNotFound(String),
944
945    #[error("Insufficient capacity in capacity reservation '{0}': have {1}, requested {2}")]
946    InsufficientCapacityReservationCapacity(String, i32, i32),
947
948    // --- Group 11 errors ---
949    #[error("The TGW multicast domain '{0}' does not exist")]
950    InvalidTgwMulticastDomainNotFound(String),
951
952    #[error("The TGW multicast domain '{0}' is in use and cannot be deleted")]
953    TgwMulticastDomainInUse(String),
954
955    #[error("The TGW multicast domain association (domain '{0}', attachment '{1}') does not exist")]
956    InvalidTgwMulticastDomainAssociationNotFound(String, String),
957
958    #[error("The TGW multicast group member '{0}' does not exist")]
959    InvalidTgwMulticastGroupMemberNotFound(String),
960
961    #[error("The TGW multicast group source '{0}' does not exist")]
962    InvalidTgwMulticastGroupSourceNotFound(String),
963
964    #[error("The TGW connect '{0}' does not exist")]
965    InvalidTgwConnectNotFound(String),
966
967    #[error("The TGW connect '{0}' has dependent connect peers")]
968    TgwConnectInUse(String),
969
970    #[error("The TGW connect peer '{0}' does not exist")]
971    InvalidTgwConnectPeerNotFound(String),
972
973    #[error("The TGW metering policy '{0}' does not exist")]
974    InvalidTgwMeteringPolicyNotFound(String),
975
976    #[error("The TGW metering policy entry '{0}' does not exist")]
977    InvalidTgwMeteringPolicyEntryNotFound(String),
978
979    #[error("The TGW policy table '{0}' does not exist")]
980    InvalidTgwPolicyTableNotFound(String),
981
982    #[error(
983        "The TGW policy table association (policy table '{0}', attachment '{1}') does not exist"
984    )]
985    InvalidTgwPolicyTableAssociationNotFound(String, String),
986
987    #[error("The TGW prefix list reference (route table '{0}', prefix list '{1}') does not exist")]
988    InvalidTgwPrefixListReferenceNotFound(String, String),
989
990    #[error("The TGW route table announcement '{0}' does not exist")]
991    InvalidTgwRouteTableAnnouncementNotFound(String),
992
993    #[error("The TGW attachment '{0}' is not in the pendingAcceptance state")]
994    TgwAttachmentNotPendingAcceptance(String),
995
996    #[error("No TGW route in route table '{0}' for destination CIDR '{1}'")]
997    TgwRouteNotFound(String, String),
998
999    // --- Group 12 errors ---
1000    #[error("The IPAM '{0}' does not exist")]
1001    InvalidIpamNotFound(String),
1002
1003    #[error("The IPAM '{0}' has dependent scopes or pools and cannot be deleted")]
1004    IpamInUse(String),
1005
1006    #[error("The IPAM scope '{0}' does not exist")]
1007    InvalidIpamScopeNotFound(String),
1008
1009    #[error("The IPAM scope '{0}' has dependent pools and cannot be deleted")]
1010    IpamScopeInUse(String),
1011
1012    #[error("The IPAM scope '{0}' is the default scope and cannot be deleted")]
1013    IpamScopeIsDefault(String),
1014
1015    #[error("The IPAM pool '{0}' does not exist")]
1016    InvalidIpamPoolNotFound(String),
1017
1018    #[error("The IPAM pool '{0}' has dependent allocations and cannot be deleted")]
1019    IpamPoolInUse(String),
1020
1021    #[error("The IPAM pool CIDR ({0}, {1}) does not exist")]
1022    InvalidIpamPoolCidrNotFound(String, String),
1023
1024    #[error("The IPAM pool allocation ({0}, {1}) does not exist")]
1025    InvalidIpamPoolAllocationNotFound(String, String),
1026
1027    #[error("The IPAM resource discovery '{0}' does not exist")]
1028    InvalidIpamResourceDiscoveryNotFound(String),
1029
1030    #[error("The IPAM resource discovery '{0}' is in use and cannot be deleted")]
1031    IpamResourceDiscoveryInUse(String),
1032
1033    #[error("The IPAM resource discovery association '{0}' does not exist")]
1034    InvalidIpamResourceDiscoveryAssociationNotFound(String),
1035
1036    #[error("The IPAM BYO-ASN ({0}, {1}) does not exist")]
1037    InvalidIpamByoasnNotFound(String, String),
1038
1039    #[error("The IPAM BYO-ASN '{0}' is already associated with a CIDR")]
1040    IpamByoasnAlreadyAssociated(String),
1041
1042    #[error("The IPAM external resource verification token '{0}' does not exist")]
1043    InvalidIpamExternalResourceVerificationTokenNotFound(String),
1044
1045    #[error("The IPAM policy '{0}' does not exist")]
1046    InvalidIpamPolicyNotFound(String),
1047
1048    #[error("The IPAM prefix list resolver '{0}' does not exist")]
1049    InvalidIpamPrefixListResolverNotFound(String),
1050
1051    #[error("The IPAM prefix list resolver '{0}' has dependent targets and cannot be deleted")]
1052    IpamPrefixListResolverInUse(String),
1053
1054    #[error("The IPAM prefix list resolver target ({0}, {1}) does not exist")]
1055    InvalidIpamPrefixListResolverTargetNotFound(String, String),
1056
1057    // --- Batch B errors ---
1058    #[error("The volume '{0}' has no modifications")]
1059    InvalidVolumeNotFound(String),
1060
1061    #[error("The bundle task '{0}' does not exist")]
1062    InvalidBundleTaskNotFound(String),
1063
1064    #[error("The import volume task '{0}' does not exist")]
1065    InvalidImportVolumeTaskNotFound(String),
1066
1067    #[error("The export image task '{0}' does not exist")]
1068    InvalidExportImageTaskNotFound(String),
1069
1070    #[error("The outpost LAG '{0}' does not exist")]
1071    InvalidOutpostLagNotFound(String),
1072}
1073
1074impl Ec2State {
1075    fn next_vpc_id(&mut self) -> String {
1076        self.counters.vpc += 1;
1077        format!("vpc-{:08x}", self.counters.vpc)
1078    }
1079
1080    fn next_subnet_id(&mut self) -> String {
1081        self.counters.subnet += 1;
1082        format!("subnet-{:08x}", self.counters.subnet)
1083    }
1084
1085    fn next_igw_id(&mut self) -> String {
1086        self.counters.igw += 1;
1087        format!("igw-{:08x}", self.counters.igw)
1088    }
1089
1090    fn next_sg_id(&mut self) -> String {
1091        self.counters.sg += 1;
1092        format!("sg-{:08x}", self.counters.sg)
1093    }
1094
1095    pub fn next_sgr_id(&mut self) -> String {
1096        self.counters.sgr += 1;
1097        format!("sgr-{:08x}", self.counters.sgr)
1098    }
1099
1100    fn next_rtb_id(&mut self) -> String {
1101        self.counters.rtb += 1;
1102        format!("rtb-{:08x}", self.counters.rtb)
1103    }
1104
1105    fn next_rtbassoc_id(&mut self) -> String {
1106        self.counters.rtbassoc += 1;
1107        format!("rtbassoc-{:08x}", self.counters.rtbassoc)
1108    }
1109
1110    fn next_keypair_id(&mut self) -> String {
1111        self.counters.keypair += 1;
1112        format!("key-{:08x}", self.counters.keypair)
1113    }
1114
1115    fn next_nacl_id(&mut self) -> String {
1116        self.counters.nacl += 1;
1117        format!("acl-{:08x}", self.counters.nacl)
1118    }
1119
1120    fn next_nacl_assoc_id(&mut self) -> String {
1121        self.counters.nacl_assoc += 1;
1122        format!("aclassoc-{:08x}", self.counters.nacl_assoc)
1123    }
1124
1125    fn next_eip_id(&mut self) -> String {
1126        self.counters.eip += 1;
1127        format!("eipalloc-{:08x}", self.counters.eip)
1128    }
1129
1130    fn next_nat_id(&mut self) -> String {
1131        self.counters.nat += 1;
1132        format!("nat-{:08x}", self.counters.nat)
1133    }
1134
1135    fn next_dopt_id(&mut self) -> String {
1136        self.counters.dopt += 1;
1137        format!("dopt-{:08x}", self.counters.dopt)
1138    }
1139
1140    fn next_tgw_id(&mut self) -> String {
1141        self.counters.tgw += 1;
1142        format!("tgw-{:08x}", self.counters.tgw)
1143    }
1144
1145    fn next_tgw_attach_id(&mut self) -> String {
1146        self.counters.tgw_attach += 1;
1147        format!("tgw-attach-{:08x}", self.counters.tgw_attach)
1148    }
1149
1150    fn next_tgw_rtb_id(&mut self) -> String {
1151        self.counters.tgw_rtb += 1;
1152        format!("tgw-rtb-{:08x}", self.counters.tgw_rtb)
1153    }
1154
1155    pub fn next_tgw_multicast_domain_id(&mut self) -> String {
1156        self.counters.tgw_multicast_domain += 1;
1157        format!(
1158            "tgw-mcast-domain-{:08x}",
1159            self.counters.tgw_multicast_domain
1160        )
1161    }
1162
1163    pub fn next_tgw_connect_id(&mut self) -> String {
1164        // Connect attachments share the tgw-attach- ID prefix in real AWS;
1165        // we mirror that and bump only the tgw_attach counter.
1166        self.counters.tgw_connect = self.counters.tgw_connect.wrapping_add(1);
1167        self.next_tgw_attach_id()
1168    }
1169
1170    pub fn next_tgw_connect_peer_id(&mut self) -> String {
1171        self.counters.tgw_connect_peer += 1;
1172        format!("tgw-connect-peer-{:08x}", self.counters.tgw_connect_peer)
1173    }
1174
1175    pub fn next_tgw_metering_policy_id(&mut self) -> String {
1176        self.counters.tgw_metering_policy += 1;
1177        format!("tgw-mp-{:08x}", self.counters.tgw_metering_policy)
1178    }
1179
1180    pub fn next_tgw_metering_policy_entry_id(&mut self) -> String {
1181        self.counters.tgw_metering_policy_entry += 1;
1182        format!(
1183            "tgw-mp-entry-{:08x}",
1184            self.counters.tgw_metering_policy_entry
1185        )
1186    }
1187
1188    pub fn next_tgw_policy_table_id(&mut self) -> String {
1189        self.counters.tgw_policy_table += 1;
1190        format!("tgw-rtb-policy-{:08x}", self.counters.tgw_policy_table)
1191    }
1192
1193    pub fn next_tgw_route_table_announcement_id(&mut self) -> String {
1194        self.counters.tgw_route_table_announcement += 1;
1195        format!(
1196            "tgw-rtb-ann-{:08x}",
1197            self.counters.tgw_route_table_announcement
1198        )
1199    }
1200
1201    fn next_instance_id(&mut self) -> String {
1202        self.counters.instance += 1;
1203        format!("i-{:08x}", self.counters.instance)
1204    }
1205
1206    fn next_vol_id(&mut self) -> String {
1207        self.counters.vol += 1;
1208        format!("vol-{:08x}", self.counters.vol)
1209    }
1210
1211    fn next_snapshot_id(&mut self) -> String {
1212        self.counters.snapshot += 1;
1213        format!("snap-{:08x}", self.counters.snapshot)
1214    }
1215
1216    fn next_ami_id(&mut self) -> String {
1217        self.counters.ami += 1;
1218        format!("ami-{:08x}", self.counters.ami)
1219    }
1220
1221    fn next_lt_id(&mut self) -> String {
1222        self.counters.lt += 1;
1223        format!("lt-{:08x}", self.counters.lt)
1224    }
1225
1226    fn next_spot_id(&mut self) -> String {
1227        self.counters.spot += 1;
1228        format!("sir-{:08x}", self.counters.spot)
1229    }
1230
1231    fn next_iam_assoc_id(&mut self) -> String {
1232        self.counters.iam_assoc += 1;
1233        format!("iip-assoc-{:08x}", self.counters.iam_assoc)
1234    }
1235
1236    fn next_host_id(&mut self) -> String {
1237        self.counters.host += 1;
1238        format!("h-{:08x}", self.counters.host)
1239    }
1240
1241    fn next_fleet_id(&mut self) -> String {
1242        self.counters.fleet += 1;
1243        format!("fleet-{:08x}", self.counters.fleet)
1244    }
1245
1246    fn next_vpce_svc_id(&mut self) -> String {
1247        self.counters.vpce_svc += 1;
1248        format!("vpce-svc-{:08x}", self.counters.vpce_svc)
1249    }
1250
1251    fn next_spot_fleet_id(&mut self) -> String {
1252        self.counters.spot_fleet += 1;
1253        format!("sfr-{:08x}", self.counters.spot_fleet)
1254    }
1255
1256    fn next_subnet_cidr_res_id(&mut self) -> String {
1257        self.counters.subnet_cidr_res += 1;
1258        format!("scr-{:08x}", self.counters.subnet_cidr_res)
1259    }
1260
1261    fn next_subnet_ipv6_assoc_id(&mut self) -> String {
1262        self.counters.subnet_ipv6_assoc += 1;
1263        format!("subnet-cidr-assoc-{:08x}", self.counters.subnet_ipv6_assoc)
1264    }
1265
1266    // --- Network ACL operations ---
1267
1268    pub fn create_network_acl(
1269        &mut self,
1270        vpc_id: &str,
1271        tags: Tags,
1272    ) -> Result<&NetworkAcl, Ec2Error> {
1273        if !self.vpcs.contains_key(vpc_id) {
1274            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
1275        }
1276        let nacl_id = self.next_nacl_id();
1277        let nacl = NetworkAcl {
1278            network_acl_id: nacl_id.clone(),
1279            vpc_id: vpc_id.to_string(),
1280            is_default: false,
1281            entries: vec![
1282                // Default deny-all inbound
1283                NetworkAclEntry {
1284                    rule_number: 32767,
1285                    protocol: "-1".to_string(),
1286                    rule_action: "deny".to_string(),
1287                    egress: false,
1288                    cidr_block: Some("0.0.0.0/0".to_string()),
1289                    ipv6_cidr_block: None,
1290                    port_range: None,
1291                    icmp_type_code: None,
1292                },
1293                // Default deny-all outbound
1294                NetworkAclEntry {
1295                    rule_number: 32767,
1296                    protocol: "-1".to_string(),
1297                    rule_action: "deny".to_string(),
1298                    egress: true,
1299                    cidr_block: Some("0.0.0.0/0".to_string()),
1300                    ipv6_cidr_block: None,
1301                    port_range: None,
1302                    icmp_type_code: None,
1303                },
1304            ],
1305            associations: Vec::new(),
1306            tags,
1307        };
1308        self.network_acls.insert(nacl_id.clone(), nacl);
1309        Ok(self.network_acls.get(&nacl_id).unwrap())
1310    }
1311
1312    pub fn delete_network_acl(&mut self, nacl_id: &str) -> Result<(), Ec2Error> {
1313        let nacl = self
1314            .network_acls
1315            .get(nacl_id)
1316            .ok_or_else(|| Ec2Error::NetworkAclNotFound(nacl_id.to_string()))?;
1317        if nacl.is_default {
1318            return Err(Ec2Error::CannotDeleteDefaultNetworkAcl);
1319        }
1320        self.network_acls.remove(nacl_id);
1321        Ok(())
1322    }
1323
1324    pub fn create_network_acl_entry(
1325        &mut self,
1326        nacl_id: &str,
1327        entry: NetworkAclEntry,
1328    ) -> Result<(), Ec2Error> {
1329        let nacl = self
1330            .network_acls
1331            .get_mut(nacl_id)
1332            .ok_or_else(|| Ec2Error::NetworkAclNotFound(nacl_id.to_string()))?;
1333        nacl.entries.push(entry);
1334        Ok(())
1335    }
1336
1337    pub fn delete_network_acl_entry(
1338        &mut self,
1339        nacl_id: &str,
1340        rule_number: i32,
1341        egress: bool,
1342    ) -> Result<(), Ec2Error> {
1343        let nacl = self
1344            .network_acls
1345            .get_mut(nacl_id)
1346            .ok_or_else(|| Ec2Error::NetworkAclNotFound(nacl_id.to_string()))?;
1347        nacl.entries
1348            .retain(|e| !(e.rule_number == rule_number && e.egress == egress));
1349        Ok(())
1350    }
1351
1352    pub fn replace_network_acl_entry(
1353        &mut self,
1354        nacl_id: &str,
1355        entry: NetworkAclEntry,
1356    ) -> Result<(), Ec2Error> {
1357        let nacl = self
1358            .network_acls
1359            .get_mut(nacl_id)
1360            .ok_or_else(|| Ec2Error::NetworkAclNotFound(nacl_id.to_string()))?;
1361        nacl.entries
1362            .retain(|e| !(e.rule_number == entry.rule_number && e.egress == entry.egress));
1363        nacl.entries.push(entry);
1364        Ok(())
1365    }
1366
1367    pub fn replace_network_acl_association(
1368        &mut self,
1369        assoc_id: &str,
1370        new_nacl_id: &str,
1371    ) -> Result<String, Ec2Error> {
1372        if !self.network_acls.contains_key(new_nacl_id) {
1373            return Err(Ec2Error::NetworkAclNotFound(new_nacl_id.to_string()));
1374        }
1375        // Find and remove the association from its current nacl
1376        let mut found_subnet_id: Option<String> = None;
1377        for nacl in self.network_acls.values_mut() {
1378            if let Some(pos) = nacl
1379                .associations
1380                .iter()
1381                .position(|a| a.network_acl_association_id == assoc_id)
1382            {
1383                found_subnet_id = Some(nacl.associations[pos].subnet_id.clone());
1384                nacl.associations.remove(pos);
1385                break;
1386            }
1387        }
1388        let subnet_id =
1389            found_subnet_id.ok_or_else(|| Ec2Error::AssociationNotFound(assoc_id.to_string()))?;
1390        let new_assoc_id = self.next_nacl_assoc_id();
1391        let new_nacl = self.network_acls.get_mut(new_nacl_id).unwrap();
1392        new_nacl.associations.push(NetworkAclAssociation {
1393            network_acl_association_id: new_assoc_id.clone(),
1394            network_acl_id: new_nacl_id.to_string(),
1395            subnet_id,
1396        });
1397        Ok(new_assoc_id)
1398    }
1399
1400    // --- Elastic IP operations ---
1401
1402    pub fn allocate_address(&mut self, tags: Tags) -> &ElasticIp {
1403        let alloc_id = self.next_eip_id();
1404        let count = self.counters.eip;
1405        let eip = ElasticIp {
1406            allocation_id: alloc_id.clone(),
1407            public_ip: format!(
1408                "54.{}.{}.{}",
1409                (count >> 16) & 0xff,
1410                (count >> 8) & 0xff,
1411                count & 0xff
1412            ),
1413            association_id: None,
1414            instance_id: None,
1415            network_interface_id: None,
1416            private_ip_address: None,
1417            address_attribute_ptr_record: None,
1418            domain: "vpc".to_string(),
1419            pending_transfer: None,
1420            tags,
1421        };
1422        self.elastic_ips.insert(alloc_id.clone(), eip);
1423        self.elastic_ips.get(&alloc_id).unwrap()
1424    }
1425
1426    pub fn release_address(&mut self, allocation_id: &str) -> Result<(), Ec2Error> {
1427        if self.elastic_ips.remove(allocation_id).is_none() {
1428            return Err(Ec2Error::AllocationNotFound(allocation_id.to_string()));
1429        }
1430        Ok(())
1431    }
1432
1433    pub fn associate_address(
1434        &mut self,
1435        allocation_id: &str,
1436        instance_id: Option<String>,
1437        network_interface_id: Option<String>,
1438        private_ip: Option<String>,
1439    ) -> Result<String, Ec2Error> {
1440        if !self.elastic_ips.contains_key(allocation_id) {
1441            return Err(Ec2Error::AllocationNotFound(allocation_id.to_string()));
1442        }
1443        self.counters.eip_assoc += 1;
1444        let assoc_id = format!("eipassoc-{:08x}", self.counters.eip_assoc);
1445        let eip = self.elastic_ips.get_mut(allocation_id).unwrap();
1446        eip.association_id = Some(assoc_id.clone());
1447        eip.instance_id = instance_id;
1448        eip.network_interface_id = network_interface_id;
1449        eip.private_ip_address = private_ip;
1450        Ok(assoc_id)
1451    }
1452
1453    pub fn disassociate_address(&mut self, association_id: &str) -> Result<(), Ec2Error> {
1454        for eip in self.elastic_ips.values_mut() {
1455            if eip.association_id.as_deref() == Some(association_id) {
1456                eip.association_id = None;
1457                eip.instance_id = None;
1458                eip.network_interface_id = None;
1459                eip.private_ip_address = None;
1460                return Ok(());
1461            }
1462        }
1463        Err(Ec2Error::AssociationIdNotFound(association_id.to_string()))
1464    }
1465
1466    // --- NAT Gateway operations ---
1467
1468    pub fn create_nat_gateway(
1469        &mut self,
1470        subnet_id: &str,
1471        connectivity_type: &str,
1472        allocation_id: Option<String>,
1473        tags: Tags,
1474    ) -> Result<&NatGateway, Ec2Error> {
1475        let subnet = self
1476            .subnets
1477            .get(subnet_id)
1478            .ok_or_else(|| Ec2Error::SubnetNotFound(subnet_id.to_string()))?;
1479        let vpc_id = subnet.vpc_id.clone();
1480        let nat_id = self.next_nat_id();
1481        let public_ip = if connectivity_type == "public" {
1482            allocation_id
1483                .as_ref()
1484                .and_then(|id| self.elastic_ips.get(id).map(|e| e.public_ip.clone()))
1485        } else {
1486            None
1487        };
1488        let nat = NatGateway {
1489            nat_gateway_id: nat_id.clone(),
1490            vpc_id,
1491            subnet_id: subnet_id.to_string(),
1492            state: "available".to_string(),
1493            connectivity_type: connectivity_type.to_string(),
1494            allocation_id,
1495            public_ip,
1496            secondary_addresses: Vec::new(),
1497            tags,
1498        };
1499        self.nat_gateways.insert(nat_id.clone(), nat);
1500        Ok(self.nat_gateways.get(&nat_id).unwrap())
1501    }
1502
1503    pub fn delete_nat_gateway(&mut self, nat_id: &str) -> Result<(), Ec2Error> {
1504        let nat = self
1505            .nat_gateways
1506            .get_mut(nat_id)
1507            .ok_or_else(|| Ec2Error::NatGatewayNotFound(nat_id.to_string()))?;
1508        nat.state = "deleted".to_string();
1509        Ok(())
1510    }
1511
1512    // --- DHCP Options operations ---
1513
1514    pub fn create_dhcp_options(
1515        &mut self,
1516        configurations: Vec<DhcpConfiguration>,
1517        tags: Tags,
1518    ) -> &DhcpOptions {
1519        let dopt_id = self.next_dopt_id();
1520        let dopt = DhcpOptions {
1521            dhcp_options_id: dopt_id.clone(),
1522            configurations,
1523            tags,
1524        };
1525        self.dhcp_options.insert(dopt_id.clone(), dopt);
1526        self.dhcp_options.get(&dopt_id).unwrap()
1527    }
1528
1529    pub fn delete_dhcp_options(&mut self, dopt_id: &str) -> Result<(), Ec2Error> {
1530        // Check it's not associated with any VPC
1531        for vpc in self.vpcs.values() {
1532            if vpc.dhcp_options_id == dopt_id {
1533                return Err(Ec2Error::DhcpOptionsAssociatedWithVpc);
1534            }
1535        }
1536        if self.dhcp_options.remove(dopt_id).is_none() {
1537            return Err(Ec2Error::DhcpOptionsNotFound(dopt_id.to_string()));
1538        }
1539        Ok(())
1540    }
1541
1542    pub fn associate_dhcp_options(&mut self, vpc_id: &str, dopt_id: &str) -> Result<(), Ec2Error> {
1543        if dopt_id != "default" && !self.dhcp_options.contains_key(dopt_id) {
1544            return Err(Ec2Error::DhcpOptionsNotFound(dopt_id.to_string()));
1545        }
1546        let vpc = self
1547            .vpcs
1548            .get_mut(vpc_id)
1549            .ok_or_else(|| Ec2Error::VpcNotFound(vpc_id.to_string()))?;
1550        vpc.dhcp_options_id = dopt_id.to_string();
1551        Ok(())
1552    }
1553
1554    pub fn create_vpc(
1555        &mut self,
1556        cidr_block: &str,
1557        instance_tenancy: &str,
1558        tags: Tags,
1559    ) -> Result<&Vpc, Ec2Error> {
1560        let vpc_id = self.next_vpc_id();
1561        let dhcp_options_id = format!("dopt-{:08x}", self.counters.vpc);
1562        let vpc = Vpc {
1563            vpc_id: vpc_id.clone(),
1564            cidr_block: cidr_block.to_string(),
1565            state: "available".to_string(),
1566            dhcp_options_id,
1567            instance_tenancy: instance_tenancy.to_string(),
1568            is_default: false,
1569            enable_dns_hostnames: false,
1570            enable_dns_support: true,
1571            secondary_cidr_blocks: Vec::new(),
1572            tags,
1573            classic_link_enabled: false,
1574        };
1575        self.vpcs.insert(vpc_id.clone(), vpc);
1576        // Create default NetworkAcl for this VPC
1577        let nacl_id = self.next_nacl_id();
1578        let default_nacl = NetworkAcl {
1579            network_acl_id: nacl_id.clone(),
1580            vpc_id: vpc_id.clone(),
1581            is_default: true,
1582            entries: vec![
1583                // Allow-all inbound (rule 100)
1584                NetworkAclEntry {
1585                    rule_number: 100,
1586                    protocol: "-1".to_string(),
1587                    rule_action: "allow".to_string(),
1588                    egress: false,
1589                    cidr_block: Some("0.0.0.0/0".to_string()),
1590                    ipv6_cidr_block: None,
1591                    port_range: None,
1592                    icmp_type_code: None,
1593                },
1594                // Default deny-all inbound (32767)
1595                NetworkAclEntry {
1596                    rule_number: 32767,
1597                    protocol: "-1".to_string(),
1598                    rule_action: "deny".to_string(),
1599                    egress: false,
1600                    cidr_block: Some("0.0.0.0/0".to_string()),
1601                    ipv6_cidr_block: None,
1602                    port_range: None,
1603                    icmp_type_code: None,
1604                },
1605                // Allow-all outbound (rule 100)
1606                NetworkAclEntry {
1607                    rule_number: 100,
1608                    protocol: "-1".to_string(),
1609                    rule_action: "allow".to_string(),
1610                    egress: true,
1611                    cidr_block: Some("0.0.0.0/0".to_string()),
1612                    ipv6_cidr_block: None,
1613                    port_range: None,
1614                    icmp_type_code: None,
1615                },
1616                // Default deny-all outbound (32767)
1617                NetworkAclEntry {
1618                    rule_number: 32767,
1619                    protocol: "-1".to_string(),
1620                    rule_action: "deny".to_string(),
1621                    egress: true,
1622                    cidr_block: Some("0.0.0.0/0".to_string()),
1623                    ipv6_cidr_block: None,
1624                    port_range: None,
1625                    icmp_type_code: None,
1626                },
1627            ],
1628            associations: Vec::new(),
1629            tags: Tags::new(),
1630        };
1631        self.network_acls.insert(nacl_id, default_nacl);
1632        Ok(self.vpcs.get(&vpc_id).unwrap())
1633    }
1634
1635    pub fn delete_vpc(&mut self, vpc_id: &str) -> Result<(), Ec2Error> {
1636        if self.vpcs.remove(vpc_id).is_none() {
1637            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
1638        }
1639        Ok(())
1640    }
1641
1642    pub fn modify_vpc_attribute(
1643        &mut self,
1644        vpc_id: &str,
1645        attribute: &str,
1646        value: bool,
1647    ) -> Result<(), Ec2Error> {
1648        let vpc = self
1649            .vpcs
1650            .get_mut(vpc_id)
1651            .ok_or_else(|| Ec2Error::VpcNotFound(vpc_id.to_string()))?;
1652        match attribute {
1653            "enableDnsHostnames" => vpc.enable_dns_hostnames = value,
1654            "enableDnsSupport" => vpc.enable_dns_support = value,
1655            _ => {}
1656        }
1657        Ok(())
1658    }
1659
1660    pub fn create_subnet(
1661        &mut self,
1662        vpc_id: &str,
1663        cidr_block: &str,
1664        availability_zone: &str,
1665        tags: Tags,
1666    ) -> Result<&Subnet, Ec2Error> {
1667        if !self.vpcs.contains_key(vpc_id) {
1668            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
1669        }
1670        let subnet_id = self.next_subnet_id();
1671        let subnet = Subnet {
1672            subnet_id: subnet_id.clone(),
1673            vpc_id: vpc_id.to_string(),
1674            cidr_block: cidr_block.to_string(),
1675            availability_zone: availability_zone.to_string(),
1676            state: "available".to_string(),
1677            available_ip_address_count: 251,
1678            map_public_ip_on_launch: false,
1679            ipv6_cidr_blocks: Vec::new(),
1680            tags,
1681        };
1682        self.subnets.insert(subnet_id.clone(), subnet);
1683        // Auto-associate with VPC's default NetworkAcl
1684        let nacl_id_opt = self
1685            .network_acls
1686            .values()
1687            .find(|nacl| nacl.vpc_id == vpc_id && nacl.is_default)
1688            .map(|nacl| nacl.network_acl_id.clone());
1689        if let Some(nacl_id) = nacl_id_opt {
1690            let assoc_id = self.next_nacl_assoc_id();
1691            if let Some(nacl) = self.network_acls.get_mut(&nacl_id) {
1692                nacl.associations.push(NetworkAclAssociation {
1693                    network_acl_association_id: assoc_id,
1694                    network_acl_id: nacl_id.clone(),
1695                    subnet_id: subnet_id.clone(),
1696                });
1697            }
1698        }
1699        Ok(self.subnets.get(&subnet_id).unwrap())
1700    }
1701
1702    pub fn modify_subnet_attribute(
1703        &mut self,
1704        subnet_id: &str,
1705        map_public_ip_on_launch: bool,
1706    ) -> Result<(), Ec2Error> {
1707        let subnet = self
1708            .subnets
1709            .get_mut(subnet_id)
1710            .ok_or_else(|| Ec2Error::SubnetNotFound(subnet_id.to_string()))?;
1711        subnet.map_public_ip_on_launch = map_public_ip_on_launch;
1712        Ok(())
1713    }
1714
1715    pub fn delete_subnet(&mut self, subnet_id: &str) -> Result<(), Ec2Error> {
1716        if self.subnets.remove(subnet_id).is_none() {
1717            return Err(Ec2Error::SubnetNotFound(subnet_id.to_string()));
1718        }
1719        Ok(())
1720    }
1721
1722    pub fn create_internet_gateway(&mut self, tags: Tags) -> &InternetGateway {
1723        let igw_id = self.next_igw_id();
1724        let igw = InternetGateway {
1725            igw_id: igw_id.clone(),
1726            attachments: Vec::new(),
1727            tags,
1728        };
1729        self.igws.insert(igw_id.clone(), igw);
1730        self.igws.get(&igw_id).unwrap()
1731    }
1732
1733    pub fn attach_internet_gateway(&mut self, igw_id: &str, vpc_id: &str) -> Result<(), Ec2Error> {
1734        if !self.vpcs.contains_key(vpc_id) {
1735            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
1736        }
1737        let igw = self
1738            .igws
1739            .get_mut(igw_id)
1740            .ok_or_else(|| Ec2Error::InternetGatewayNotFound(igw_id.to_string()))?;
1741        igw.attachments.push(IgwAttachment {
1742            vpc_id: vpc_id.to_string(),
1743            state: "available".to_string(),
1744        });
1745        Ok(())
1746    }
1747
1748    pub fn detach_internet_gateway(&mut self, igw_id: &str, vpc_id: &str) -> Result<(), Ec2Error> {
1749        let igw = self
1750            .igws
1751            .get_mut(igw_id)
1752            .ok_or_else(|| Ec2Error::InternetGatewayNotFound(igw_id.to_string()))?;
1753        igw.attachments.retain(|a| a.vpc_id != vpc_id);
1754        Ok(())
1755    }
1756
1757    pub fn delete_internet_gateway(&mut self, igw_id: &str) -> Result<(), Ec2Error> {
1758        if self.igws.remove(igw_id).is_none() {
1759            return Err(Ec2Error::InternetGatewayNotFound(igw_id.to_string()));
1760        }
1761        Ok(())
1762    }
1763
1764    pub fn create_security_group(
1765        &mut self,
1766        group_name: &str,
1767        description: &str,
1768        vpc_id: &str,
1769        owner_id: &str,
1770        tags: Tags,
1771    ) -> Result<&SecurityGroup, Ec2Error> {
1772        if !self.vpcs.contains_key(vpc_id) {
1773            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
1774        }
1775        let group_id = self.next_sg_id();
1776        let sg = SecurityGroup {
1777            group_id: group_id.clone(),
1778            group_name: group_name.to_string(),
1779            description: description.to_string(),
1780            vpc_id: vpc_id.to_string(),
1781            owner_id: owner_id.to_string(),
1782            ingress_rules: Vec::new(),
1783            egress_rules: vec![IpPermission {
1784                rule_id: self.next_sgr_id(),
1785                from_port: None,
1786                to_port: None,
1787                ip_protocol: "-1".to_string(),
1788                ip_ranges: vec![IpRange {
1789                    cidr_ip: "0.0.0.0/0".to_string(),
1790                    description: None,
1791                }],
1792                ipv6_ranges: Vec::new(),
1793                user_id_group_pairs: Vec::new(),
1794            }],
1795            tags,
1796        };
1797        self.security_groups.insert(group_id.clone(), sg);
1798        Ok(self.security_groups.get(&group_id).unwrap())
1799    }
1800
1801    pub fn authorize_security_group_ingress(
1802        &mut self,
1803        group_id: &str,
1804        rules: Vec<IpPermission>,
1805    ) -> Result<(), Ec2Error> {
1806        let rules_with_ids: Vec<IpPermission> = rules
1807            .into_iter()
1808            .map(|mut r| {
1809                if r.rule_id.is_empty() {
1810                    r.rule_id = self.next_sgr_id();
1811                }
1812                r
1813            })
1814            .collect();
1815        let sg = self
1816            .security_groups
1817            .get_mut(group_id)
1818            .ok_or_else(|| Ec2Error::SecurityGroupNotFound(group_id.to_string()))?;
1819        sg.ingress_rules.extend(rules_with_ids);
1820        Ok(())
1821    }
1822
1823    pub fn authorize_security_group_egress(
1824        &mut self,
1825        group_id: &str,
1826        rules: Vec<IpPermission>,
1827    ) -> Result<(), Ec2Error> {
1828        let rules_with_ids: Vec<IpPermission> = rules
1829            .into_iter()
1830            .map(|mut r| {
1831                if r.rule_id.is_empty() {
1832                    r.rule_id = self.next_sgr_id();
1833                }
1834                r
1835            })
1836            .collect();
1837        let sg = self
1838            .security_groups
1839            .get_mut(group_id)
1840            .ok_or_else(|| Ec2Error::SecurityGroupNotFound(group_id.to_string()))?;
1841        sg.egress_rules.extend(rules_with_ids);
1842        Ok(())
1843    }
1844
1845    pub fn revoke_security_group_ingress(
1846        &mut self,
1847        group_id: &str,
1848        rules: &[IpPermission],
1849    ) -> Result<(), Ec2Error> {
1850        let sg = self
1851            .security_groups
1852            .get_mut(group_id)
1853            .ok_or_else(|| Ec2Error::SecurityGroupNotFound(group_id.to_string()))?;
1854        for rule in rules {
1855            sg.ingress_rules.retain(|r| {
1856                r.ip_protocol != rule.ip_protocol
1857                    || r.from_port != rule.from_port
1858                    || r.to_port != rule.to_port
1859            });
1860        }
1861        Ok(())
1862    }
1863
1864    pub fn revoke_security_group_egress(
1865        &mut self,
1866        group_id: &str,
1867        rules: &[IpPermission],
1868    ) -> Result<(), Ec2Error> {
1869        let sg = self
1870            .security_groups
1871            .get_mut(group_id)
1872            .ok_or_else(|| Ec2Error::SecurityGroupNotFound(group_id.to_string()))?;
1873        for rule in rules {
1874            sg.egress_rules.retain(|r| {
1875                r.ip_protocol != rule.ip_protocol
1876                    || r.from_port != rule.from_port
1877                    || r.to_port != rule.to_port
1878            });
1879        }
1880        Ok(())
1881    }
1882
1883    pub fn delete_security_group(&mut self, group_id: &str) -> Result<(), Ec2Error> {
1884        if self.security_groups.remove(group_id).is_none() {
1885            return Err(Ec2Error::SecurityGroupNotFound(group_id.to_string()));
1886        }
1887        Ok(())
1888    }
1889
1890    pub fn create_route_table(
1891        &mut self,
1892        vpc_id: &str,
1893        tags: Tags,
1894    ) -> Result<&RouteTable, Ec2Error> {
1895        let vpc = self
1896            .vpcs
1897            .get(vpc_id)
1898            .ok_or_else(|| Ec2Error::VpcNotFound(vpc_id.to_string()))?;
1899        let local_cidr = vpc.cidr_block.clone();
1900        let rtb_id = self.next_rtb_id();
1901        let rtb = RouteTable {
1902            route_table_id: rtb_id.clone(),
1903            vpc_id: vpc_id.to_string(),
1904            routes: vec![Route {
1905                destination_cidr_block: Some(local_cidr),
1906                destination_ipv6_cidr_block: None,
1907                gateway_id: Some("local".to_string()),
1908                state: "active".to_string(),
1909                origin: "CreateRouteTable".to_string(),
1910            }],
1911            associations: Vec::new(),
1912            propagating_vgws: Vec::new(),
1913            tags,
1914        };
1915        self.route_tables.insert(rtb_id.clone(), rtb);
1916        Ok(self.route_tables.get(&rtb_id).unwrap())
1917    }
1918
1919    pub fn associate_route_table(
1920        &mut self,
1921        rtb_id: &str,
1922        subnet_id: &str,
1923    ) -> Result<String, Ec2Error> {
1924        if !self.subnets.contains_key(subnet_id) {
1925            return Err(Ec2Error::SubnetNotFound(subnet_id.to_string()));
1926        }
1927        let assoc_id = self.next_rtbassoc_id();
1928        let rtb = self
1929            .route_tables
1930            .get_mut(rtb_id)
1931            .ok_or_else(|| Ec2Error::RouteTableNotFound(rtb_id.to_string()))?;
1932        rtb.associations.push(RouteTableAssociation {
1933            association_id: assoc_id.clone(),
1934            subnet_id: Some(subnet_id.to_string()),
1935            gateway_id: None,
1936            main: false,
1937            state: "associated".to_string(),
1938        });
1939        Ok(assoc_id)
1940    }
1941
1942    pub fn disassociate_route_table(&mut self, assoc_id: &str) -> Result<(), Ec2Error> {
1943        for rtb in self.route_tables.values_mut() {
1944            let before = rtb.associations.len();
1945            rtb.associations.retain(|a| a.association_id != assoc_id);
1946            if rtb.associations.len() < before {
1947                return Ok(());
1948            }
1949        }
1950        Err(Ec2Error::AssociationIdNotFound(assoc_id.to_string()))
1951    }
1952
1953    pub fn create_route(
1954        &mut self,
1955        rtb_id: &str,
1956        destination_cidr: Option<String>,
1957        destination_ipv6_cidr: Option<String>,
1958        gateway_id: Option<String>,
1959    ) -> Result<(), Ec2Error> {
1960        let rtb = self
1961            .route_tables
1962            .get_mut(rtb_id)
1963            .ok_or_else(|| Ec2Error::RouteTableNotFound(rtb_id.to_string()))?;
1964        rtb.routes.push(Route {
1965            destination_cidr_block: destination_cidr,
1966            destination_ipv6_cidr_block: destination_ipv6_cidr,
1967            gateway_id,
1968            state: "active".to_string(),
1969            origin: "CreateRoute".to_string(),
1970        });
1971        Ok(())
1972    }
1973
1974    pub fn delete_route(&mut self, rtb_id: &str, destination_cidr: &str) -> Result<(), Ec2Error> {
1975        let rtb = self
1976            .route_tables
1977            .get_mut(rtb_id)
1978            .ok_or_else(|| Ec2Error::RouteTableNotFound(rtb_id.to_string()))?;
1979        rtb.routes.retain(|r| {
1980            r.destination_cidr_block.as_deref() != Some(destination_cidr)
1981                && r.destination_ipv6_cidr_block.as_deref() != Some(destination_cidr)
1982        });
1983        Ok(())
1984    }
1985
1986    pub fn delete_route_table(&mut self, rtb_id: &str) -> Result<(), Ec2Error> {
1987        if self.route_tables.remove(rtb_id).is_none() {
1988            return Err(Ec2Error::RouteTableNotFound(rtb_id.to_string()));
1989        }
1990        Ok(())
1991    }
1992
1993    pub fn import_key_pair(
1994        &mut self,
1995        key_name: &str,
1996        _public_key_material: &str,
1997        tags: Tags,
1998    ) -> &KeyPair {
1999        let key_pair_id = self.next_keypair_id();
2000        let kp = KeyPair {
2001            key_pair_id: key_pair_id.clone(),
2002            key_name: key_name.to_string(),
2003            fingerprint: "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99".to_string(),
2004            tags,
2005        };
2006        self.key_pairs.insert(key_name.to_string(), kp);
2007        self.key_pairs.get(key_name).unwrap()
2008    }
2009
2010    pub fn delete_key_pair(
2011        &mut self,
2012        key_name: Option<&str>,
2013        key_pair_id: Option<&str>,
2014    ) -> Result<(), Ec2Error> {
2015        if let Some(name) = key_name {
2016            self.key_pairs.remove(name);
2017            return Ok(());
2018        }
2019        if let Some(id) = key_pair_id {
2020            let name = self
2021                .key_pairs
2022                .iter()
2023                .find(|(_, kp)| kp.key_pair_id == id)
2024                .map(|(k, _)| k.clone());
2025            if let Some(n) = name {
2026                self.key_pairs.remove(&n);
2027                return Ok(());
2028            }
2029        }
2030        Ok(())
2031    }
2032
2033    pub fn create_tags(&mut self, resource_ids: &[String], tags: &Tags) {
2034        for id in resource_ids {
2035            if let Some(r) = self.vpcs.get_mut(id.as_str()) {
2036                r.tags.extend(tags.clone());
2037            } else if let Some(r) = self.subnets.get_mut(id.as_str()) {
2038                r.tags.extend(tags.clone());
2039            } else if let Some(r) = self.igws.get_mut(id.as_str()) {
2040                r.tags.extend(tags.clone());
2041            } else if let Some(r) = self.security_groups.get_mut(id.as_str()) {
2042                r.tags.extend(tags.clone());
2043            } else if let Some(r) = self.route_tables.get_mut(id.as_str()) {
2044                r.tags.extend(tags.clone());
2045            } else {
2046                // Try key pairs by key_pair_id
2047                let name = self
2048                    .key_pairs
2049                    .iter()
2050                    .find(|(_, kp)| kp.key_pair_id == *id)
2051                    .map(|(k, _)| k.clone());
2052                if let Some(n) = name {
2053                    if let Some(kp) = self.key_pairs.get_mut(&n) {
2054                        kp.tags.extend(tags.clone());
2055                    }
2056                }
2057            }
2058        }
2059    }
2060
2061    pub fn delete_tags(&mut self, resource_ids: &[String], tag_keys: &[String]) {
2062        for id in resource_ids {
2063            let remove = |tags: &mut Tags| {
2064                for k in tag_keys {
2065                    tags.remove(k);
2066                }
2067            };
2068            if let Some(r) = self.vpcs.get_mut(id.as_str()) {
2069                remove(&mut r.tags);
2070            } else if let Some(r) = self.subnets.get_mut(id.as_str()) {
2071                remove(&mut r.tags);
2072            } else if let Some(r) = self.igws.get_mut(id.as_str()) {
2073                remove(&mut r.tags);
2074            } else if let Some(r) = self.security_groups.get_mut(id.as_str()) {
2075                remove(&mut r.tags);
2076            } else if let Some(r) = self.route_tables.get_mut(id.as_str()) {
2077                remove(&mut r.tags);
2078            }
2079        }
2080    }
2081
2082    // --- Route table extended operations ---
2083
2084    pub fn replace_route(
2085        &mut self,
2086        rtb_id: &str,
2087        destination_cidr: &str,
2088        gateway_id: Option<String>,
2089    ) -> Result<(), Ec2Error> {
2090        let rtb = self
2091            .route_tables
2092            .get_mut(rtb_id)
2093            .ok_or_else(|| Ec2Error::RouteTableNotFound(rtb_id.to_string()))?;
2094        for route in rtb.routes.iter_mut() {
2095            if route.destination_cidr_block.as_deref() == Some(destination_cidr)
2096                || route.destination_ipv6_cidr_block.as_deref() == Some(destination_cidr)
2097            {
2098                route.gateway_id = gateway_id;
2099                return Ok(());
2100            }
2101        }
2102        Err(Ec2Error::RouteNotFound(destination_cidr.to_string()))
2103    }
2104
2105    pub fn replace_route_table_association(
2106        &mut self,
2107        assoc_id: &str,
2108        new_rtb_id: &str,
2109    ) -> Result<String, Ec2Error> {
2110        if !self.route_tables.contains_key(new_rtb_id) {
2111            return Err(Ec2Error::RouteTableNotFound(new_rtb_id.to_string()));
2112        }
2113        let mut found_assoc: Option<RouteTableAssociation> = None;
2114        for rtb in self.route_tables.values_mut() {
2115            if let Some(pos) = rtb
2116                .associations
2117                .iter()
2118                .position(|a| a.association_id == assoc_id)
2119            {
2120                found_assoc = Some(rtb.associations.remove(pos));
2121                break;
2122            }
2123        }
2124        let old_assoc =
2125            found_assoc.ok_or_else(|| Ec2Error::AssociationIdNotFound(assoc_id.to_string()))?;
2126        let new_assoc_id = self.next_rtbassoc_id();
2127        let rtb = self.route_tables.get_mut(new_rtb_id).unwrap();
2128        rtb.associations.push(RouteTableAssociation {
2129            association_id: new_assoc_id.clone(),
2130            subnet_id: old_assoc.subnet_id,
2131            gateway_id: old_assoc.gateway_id,
2132            main: old_assoc.main,
2133            state: "associated".to_string(),
2134        });
2135        Ok(new_assoc_id)
2136    }
2137
2138    // --- CreateKeyPair ---
2139
2140    pub fn create_key_pair(&mut self, key_name: &str, tags: Tags) -> &KeyPair {
2141        let key_pair_id = self.next_keypair_id();
2142        let kp = KeyPair {
2143            key_pair_id: key_pair_id.clone(),
2144            key_name: key_name.to_string(),
2145            fingerprint: "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99".to_string(),
2146            tags,
2147        };
2148        self.key_pairs.insert(key_name.to_string(), kp);
2149        self.key_pairs.get(key_name).unwrap()
2150    }
2151
2152    // --- Egress-only internet gateway operations ---
2153
2154    fn next_eigw_id(&mut self) -> String {
2155        self.counters.eigw += 1;
2156        format!("eigw-{:08x}", self.counters.eigw)
2157    }
2158
2159    pub fn create_egress_only_igw(
2160        &mut self,
2161        vpc_id: &str,
2162        tags: Tags,
2163    ) -> Result<&EgressOnlyInternetGateway, Ec2Error> {
2164        if !self.vpcs.contains_key(vpc_id) {
2165            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
2166        }
2167        let eigw_id = self.next_eigw_id();
2168        let eigw = EgressOnlyInternetGateway {
2169            eigw_id: eigw_id.clone(),
2170            state: "available".to_string(),
2171            attachments: vec![EoigwAttachment {
2172                vpc_id: vpc_id.to_string(),
2173                state: "attached".to_string(),
2174            }],
2175            tags,
2176        };
2177        self.egress_only_igws.insert(eigw_id.clone(), eigw);
2178        Ok(self.egress_only_igws.get(&eigw_id).unwrap())
2179    }
2180
2181    pub fn delete_egress_only_igw(&mut self, eigw_id: &str) -> Result<(), Ec2Error> {
2182        if self.egress_only_igws.remove(eigw_id).is_none() {
2183            return Err(Ec2Error::EgressOnlyIgwNotFound(eigw_id.to_string()));
2184        }
2185        Ok(())
2186    }
2187
2188    // --- Flow log operations ---
2189
2190    fn next_flow_log_id(&mut self) -> String {
2191        self.counters.flow_log += 1;
2192        format!("fl-{:08x}", self.counters.flow_log)
2193    }
2194
2195    pub fn create_flow_log(
2196        &mut self,
2197        resource_id: &str,
2198        traffic_type: &str,
2199        log_destination_type: &str,
2200        log_destination: Option<String>,
2201        log_group_name: Option<String>,
2202        tags: Tags,
2203    ) -> String {
2204        let flow_log_id = self.next_flow_log_id();
2205        let fl = FlowLog {
2206            flow_log_id: flow_log_id.clone(),
2207            resource_id: resource_id.to_string(),
2208            traffic_type: traffic_type.to_string(),
2209            log_destination_type: log_destination_type.to_string(),
2210            log_destination,
2211            log_group_name,
2212            deliver_logs_status: "SUCCESS".to_string(),
2213            flow_log_status: "ACTIVE".to_string(),
2214            tags,
2215        };
2216        self.flow_logs.insert(flow_log_id.clone(), fl);
2217        flow_log_id
2218    }
2219
2220    pub fn delete_flow_logs(&mut self, flow_log_ids: &[String]) {
2221        for id in flow_log_ids {
2222            self.flow_logs.remove(id);
2223        }
2224    }
2225
2226    // --- VPC peering operations ---
2227
2228    fn next_peering_id(&mut self) -> String {
2229        self.counters.vpc_peering += 1;
2230        format!("pcx-{:08x}", self.counters.vpc_peering)
2231    }
2232
2233    pub fn create_vpc_peering_connection(
2234        &mut self,
2235        requester_vpc_id: &str,
2236        accepter_vpc_id: &str,
2237        tags: Tags,
2238    ) -> &VpcPeeringConnection {
2239        let peering_id = self.next_peering_id();
2240        let conn = VpcPeeringConnection {
2241            peering_id: peering_id.clone(),
2242            requester_vpc_id: requester_vpc_id.to_string(),
2243            accepter_vpc_id: Some(accepter_vpc_id.to_string()),
2244            status: "pending-acceptance".to_string(),
2245            tags,
2246            requester_peering_options: None,
2247            accepter_peering_options: None,
2248        };
2249        self.vpc_peering_connections
2250            .insert(peering_id.clone(), conn);
2251        self.vpc_peering_connections.get(&peering_id).unwrap()
2252    }
2253
2254    pub fn accept_vpc_peering_connection(&mut self, peering_id: &str) -> Result<(), Ec2Error> {
2255        let conn = self
2256            .vpc_peering_connections
2257            .get_mut(peering_id)
2258            .ok_or_else(|| Ec2Error::VpcPeeringConnectionNotFound(peering_id.to_string()))?;
2259        conn.status = "active".to_string();
2260        Ok(())
2261    }
2262
2263    pub fn reject_vpc_peering_connection(&mut self, peering_id: &str) -> Result<(), Ec2Error> {
2264        let conn = self
2265            .vpc_peering_connections
2266            .get_mut(peering_id)
2267            .ok_or_else(|| Ec2Error::VpcPeeringConnectionNotFound(peering_id.to_string()))?;
2268        conn.status = "rejected".to_string();
2269        Ok(())
2270    }
2271
2272    pub fn delete_vpc_peering_connection(&mut self, peering_id: &str) -> Result<(), Ec2Error> {
2273        let conn = self
2274            .vpc_peering_connections
2275            .get_mut(peering_id)
2276            .ok_or_else(|| Ec2Error::VpcPeeringConnectionNotFound(peering_id.to_string()))?;
2277        conn.status = "deleted".to_string();
2278        Ok(())
2279    }
2280
2281    // --- VPC endpoint operations ---
2282
2283    fn next_endpoint_id(&mut self) -> String {
2284        self.counters.vpc_endpoint += 1;
2285        format!("vpce-{:08x}", self.counters.vpc_endpoint)
2286    }
2287
2288    pub fn create_vpc_endpoint(
2289        &mut self,
2290        vpc_id: &str,
2291        service_name: &str,
2292        endpoint_type: &str,
2293        policy_document: Option<String>,
2294        route_table_ids: Vec<String>,
2295        subnet_ids: Vec<String>,
2296        security_group_ids: Vec<String>,
2297        tags: Tags,
2298    ) -> Result<&VpcEndpoint, Ec2Error> {
2299        if !self.vpcs.contains_key(vpc_id) {
2300            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
2301        }
2302        let endpoint_id = self.next_endpoint_id();
2303        // If the endpoint targets a known endpoint-service configuration,
2304        // seed a connection record so Accept/Reject can transition it. The
2305        // initial state depends on whether the service requires acceptance.
2306        let matched_service: Option<(String, bool)> = self
2307            .vpc_endpoint_service_configs
2308            .values()
2309            .find(|s| s.service_name == service_name)
2310            .map(|s| (s.service_id.clone(), s.acceptance_required));
2311        let ep = VpcEndpoint {
2312            endpoint_id: endpoint_id.clone(),
2313            vpc_id: vpc_id.to_string(),
2314            service_name: service_name.to_string(),
2315            endpoint_type: endpoint_type.to_string(),
2316            state: "available".to_string(),
2317            policy_document,
2318            route_table_ids,
2319            subnet_ids,
2320            security_group_ids,
2321            private_dns_enabled: None,
2322            tags,
2323        };
2324        self.vpc_endpoints.insert(endpoint_id.clone(), ep);
2325        if let Some((svc_id, acceptance_required)) = matched_service {
2326            let conn_state = if acceptance_required {
2327                "pendingAcceptance"
2328            } else {
2329                "available"
2330            };
2331            self.upsert_vpc_endpoint_connection(&svc_id, &endpoint_id, "000000000000", conn_state);
2332        }
2333        Ok(self.vpc_endpoints.get(&endpoint_id).unwrap())
2334    }
2335
2336    pub fn delete_vpc_endpoints(&mut self, endpoint_ids: &[String]) {
2337        for id in endpoint_ids {
2338            self.vpc_endpoints.remove(id);
2339        }
2340    }
2341
2342    // --- Managed prefix list operations ---
2343
2344    fn next_prefix_list_id(&mut self) -> String {
2345        self.counters.prefix_list += 1;
2346        format!("pl-{:08x}", self.counters.prefix_list)
2347    }
2348
2349    pub fn create_managed_prefix_list(
2350        &mut self,
2351        name: &str,
2352        max_entries: i32,
2353        address_family: &str,
2354        entries: Vec<TypesPrefixListEntry>,
2355        tags: Tags,
2356    ) -> &ManagedPrefixList {
2357        let id = self.next_prefix_list_id();
2358        let pl = ManagedPrefixList {
2359            prefix_list_id: id.clone(),
2360            prefix_list_name: name.to_string(),
2361            state: "create-complete".to_string(),
2362            address_family: address_family.to_string(),
2363            max_entries,
2364            entries: entries.clone(),
2365            tags,
2366            version: 1,
2367            version_history: vec![ManagedPrefixListVersion {
2368                version: 1,
2369                entries,
2370            }],
2371        };
2372        self.managed_prefix_lists.insert(id.clone(), pl);
2373        self.managed_prefix_lists.get(&id).unwrap()
2374    }
2375
2376    pub fn delete_managed_prefix_list(&mut self, id: &str) -> Result<&ManagedPrefixList, Ec2Error> {
2377        let pl = self
2378            .managed_prefix_lists
2379            .get_mut(id)
2380            .ok_or_else(|| Ec2Error::PrefixListNotFound(id.to_string()))?;
2381        pl.state = "delete-complete".to_string();
2382        Ok(self.managed_prefix_lists.get(id).unwrap())
2383    }
2384
2385    pub fn modify_managed_prefix_list(
2386        &mut self,
2387        id: &str,
2388        add_entries: Vec<TypesPrefixListEntry>,
2389        remove_cidrs: Vec<String>,
2390    ) -> Result<(), Ec2Error> {
2391        let pl = self
2392            .managed_prefix_lists
2393            .get_mut(id)
2394            .ok_or_else(|| Ec2Error::PrefixListNotFound(id.to_string()))?;
2395        pl.entries.retain(|e| !remove_cidrs.contains(&e.cidr));
2396        pl.entries.extend(add_entries);
2397        pl.version += 1;
2398        pl.version_history.push(ManagedPrefixListVersion {
2399            version: pl.version,
2400            entries: pl.entries.clone(),
2401        });
2402        Ok(())
2403    }
2404
2405    // --- Customer gateway operations ---
2406
2407    fn next_cgw_id(&mut self) -> String {
2408        self.counters.cgw += 1;
2409        format!("cgw-{:08x}", self.counters.cgw)
2410    }
2411
2412    pub fn create_customer_gateway(
2413        &mut self,
2414        bgp_asn: &str,
2415        ip_address: &str,
2416        gateway_type: &str,
2417        tags: Tags,
2418    ) -> &CustomerGateway {
2419        let id = self.next_cgw_id();
2420        let cgw = CustomerGateway {
2421            customer_gateway_id: id.clone(),
2422            bgp_asn: bgp_asn.to_string(),
2423            ip_address: ip_address.to_string(),
2424            gateway_type: gateway_type.to_string(),
2425            state: "available".to_string(),
2426            tags,
2427        };
2428        self.customer_gateways.insert(id.clone(), cgw);
2429        self.customer_gateways.get(&id).unwrap()
2430    }
2431
2432    pub fn delete_customer_gateway(&mut self, id: &str) -> Result<(), Ec2Error> {
2433        if self.customer_gateways.remove(id).is_none() {
2434            return Err(Ec2Error::CustomerGatewayNotFound(id.to_string()));
2435        }
2436        Ok(())
2437    }
2438
2439    // --- VPN gateway operations ---
2440
2441    fn next_vgw_id(&mut self) -> String {
2442        self.counters.vgw += 1;
2443        format!("vgw-{:08x}", self.counters.vgw)
2444    }
2445
2446    pub fn create_vpn_gateway(
2447        &mut self,
2448        gateway_type: &str,
2449        amazon_side_asn: Option<i64>,
2450        tags: Tags,
2451    ) -> &VpnGateway {
2452        let id = self.next_vgw_id();
2453        let vgw = VpnGateway {
2454            vpn_gateway_id: id.clone(),
2455            gateway_type: gateway_type.to_string(),
2456            state: "available".to_string(),
2457            amazon_side_asn,
2458            vpc_attachments: Vec::new(),
2459            tags,
2460        };
2461        self.vpn_gateways.insert(id.clone(), vgw);
2462        self.vpn_gateways.get(&id).unwrap()
2463    }
2464
2465    pub fn delete_vpn_gateway(&mut self, id: &str) -> Result<(), Ec2Error> {
2466        let vgw = self
2467            .vpn_gateways
2468            .get_mut(id)
2469            .ok_or_else(|| Ec2Error::VpnGatewayNotFound(id.to_string()))?;
2470        vgw.state = "deleted".to_string();
2471        Ok(())
2472    }
2473
2474    pub fn attach_vpn_gateway(&mut self, vgw_id: &str, vpc_id: &str) -> Result<(), Ec2Error> {
2475        if !self.vpcs.contains_key(vpc_id) {
2476            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
2477        }
2478        let vgw = self
2479            .vpn_gateways
2480            .get_mut(vgw_id)
2481            .ok_or_else(|| Ec2Error::VpnGatewayNotFound(vgw_id.to_string()))?;
2482        vgw.vpc_attachments.push(VgwVpcAttachment {
2483            vpc_id: vpc_id.to_string(),
2484            state: "attached".to_string(),
2485        });
2486        Ok(())
2487    }
2488
2489    pub fn detach_vpn_gateway(&mut self, vgw_id: &str, vpc_id: &str) -> Result<(), Ec2Error> {
2490        let vgw = self
2491            .vpn_gateways
2492            .get_mut(vgw_id)
2493            .ok_or_else(|| Ec2Error::VpnGatewayNotFound(vgw_id.to_string()))?;
2494        vgw.vpc_attachments.retain(|a| a.vpc_id != vpc_id);
2495        Ok(())
2496    }
2497
2498    // --- VPN connection operations ---
2499
2500    fn next_vpn_id(&mut self) -> String {
2501        self.counters.vpn += 1;
2502        format!("vpn-{:08x}", self.counters.vpn)
2503    }
2504
2505    pub fn create_vpn_connection(
2506        &mut self,
2507        vgw_id: &str,
2508        cgw_id: &str,
2509        connection_type: &str,
2510        tags: Tags,
2511    ) -> &VpnConnection {
2512        let id = self.next_vpn_id();
2513        let vpn = VpnConnection {
2514            vpn_connection_id: id.clone(),
2515            vpn_gateway_id: vgw_id.to_string(),
2516            customer_gateway_id: cgw_id.to_string(),
2517            transit_gateway_id: None,
2518            connection_type: connection_type.to_string(),
2519            state: "available".to_string(),
2520            tags,
2521            routes: Vec::new(),
2522            // Two pre-allocated tunnels per real VPN connection. We seed them
2523            // with deterministic outside IPs so `ModifyVpnTunnelOptions` and
2524            // `ModifyVpnTunnelCertificate` have something to address.
2525            options: Some(VpnConnectionOptions {
2526                tunnel_options: vec![
2527                    VpnTunnelOptions {
2528                        outside_ip_address: Some("203.0.113.1".to_string()),
2529                        ..Default::default()
2530                    },
2531                    VpnTunnelOptions {
2532                        outside_ip_address: Some("203.0.113.2".to_string()),
2533                        ..Default::default()
2534                    },
2535                ],
2536                ..Default::default()
2537            }),
2538            tunnel_replacement_status: None,
2539        };
2540        self.vpn_connections.insert(id.clone(), vpn);
2541        self.vpn_connections.get(&id).unwrap()
2542    }
2543
2544    pub fn delete_vpn_connection(&mut self, id: &str) -> Result<(), Ec2Error> {
2545        let vpn = self
2546            .vpn_connections
2547            .get_mut(id)
2548            .ok_or_else(|| Ec2Error::VpnConnectionNotFound(id.to_string()))?;
2549        vpn.state = "deleted".to_string();
2550        Ok(())
2551    }
2552
2553    pub fn create_vpn_connection_route(
2554        &mut self,
2555        vpn_id: &str,
2556        destination_cidr: &str,
2557    ) -> Result<(), Ec2Error> {
2558        let vpn = self
2559            .vpn_connections
2560            .get_mut(vpn_id)
2561            .ok_or_else(|| Ec2Error::VpnConnectionNotFound(vpn_id.to_string()))?;
2562        // Idempotent: if the same CIDR is added twice, just keep one entry.
2563        if !vpn
2564            .routes
2565            .iter()
2566            .any(|r| r.destination_cidr_block == destination_cidr)
2567        {
2568            vpn.routes.push(VpnStaticRoute {
2569                destination_cidr_block: destination_cidr.to_string(),
2570                source: "Static".to_string(),
2571                state: "available".to_string(),
2572            });
2573        }
2574        Ok(())
2575    }
2576
2577    pub fn delete_vpn_connection_route(
2578        &mut self,
2579        vpn_id: &str,
2580        destination_cidr: &str,
2581    ) -> Result<(), Ec2Error> {
2582        let vpn = self
2583            .vpn_connections
2584            .get_mut(vpn_id)
2585            .ok_or_else(|| Ec2Error::VpnConnectionNotFound(vpn_id.to_string()))?;
2586        let before = vpn.routes.len();
2587        vpn.routes
2588            .retain(|r| r.destination_cidr_block != destination_cidr);
2589        if vpn.routes.len() == before {
2590            return Err(Ec2Error::InvalidVpnConnectionRouteNotFound {
2591                vpn_connection_id: vpn_id.to_string(),
2592                destination_cidr: destination_cidr.to_string(),
2593            });
2594        }
2595        Ok(())
2596    }
2597
2598    pub fn modify_vpn_connection(
2599        &mut self,
2600        vpn_id: &str,
2601        new_cgw: Option<String>,
2602        new_vgw: Option<String>,
2603        new_tgw: Option<String>,
2604    ) -> Result<&VpnConnection, Ec2Error> {
2605        let vpn = self
2606            .vpn_connections
2607            .get_mut(vpn_id)
2608            .ok_or_else(|| Ec2Error::VpnConnectionNotFound(vpn_id.to_string()))?;
2609        if let Some(c) = new_cgw {
2610            vpn.customer_gateway_id = c;
2611        }
2612        if let Some(v) = new_vgw {
2613            vpn.vpn_gateway_id = v;
2614        }
2615        if let Some(t) = new_tgw {
2616            vpn.transit_gateway_id = Some(t);
2617        }
2618        Ok(self.vpn_connections.get(vpn_id).unwrap())
2619    }
2620
2621    #[allow(clippy::too_many_arguments)]
2622    pub fn modify_vpn_connection_options(
2623        &mut self,
2624        vpn_id: &str,
2625        local_ipv4: Option<String>,
2626        local_ipv6: Option<String>,
2627        remote_ipv4: Option<String>,
2628        remote_ipv6: Option<String>,
2629        tunnel_inside_ip_version: Option<String>,
2630    ) -> Result<&VpnConnection, Ec2Error> {
2631        let vpn = self
2632            .vpn_connections
2633            .get_mut(vpn_id)
2634            .ok_or_else(|| Ec2Error::VpnConnectionNotFound(vpn_id.to_string()))?;
2635        let opts = vpn
2636            .options
2637            .get_or_insert_with(VpnConnectionOptions::default);
2638        if let Some(v) = local_ipv4 {
2639            opts.local_ipv4_network_cidr = Some(v);
2640        }
2641        if let Some(v) = local_ipv6 {
2642            opts.local_ipv6_network_cidr = Some(v);
2643        }
2644        if let Some(v) = remote_ipv4 {
2645            opts.remote_ipv4_network_cidr = Some(v);
2646        }
2647        if let Some(v) = remote_ipv6 {
2648            opts.remote_ipv6_network_cidr = Some(v);
2649        }
2650        if let Some(v) = tunnel_inside_ip_version {
2651            opts.tunnel_inside_ip_version = Some(v);
2652        }
2653        Ok(self.vpn_connections.get(vpn_id).unwrap())
2654    }
2655
2656    pub fn modify_vpn_tunnel_certificate(
2657        &mut self,
2658        vpn_id: &str,
2659        outside_ip: &str,
2660    ) -> Result<&VpnConnection, Ec2Error> {
2661        let vpn = self
2662            .vpn_connections
2663            .get_mut(vpn_id)
2664            .ok_or_else(|| Ec2Error::VpnConnectionNotFound(vpn_id.to_string()))?;
2665        let opts = vpn
2666            .options
2667            .get_or_insert_with(VpnConnectionOptions::default);
2668        let tunnel = opts
2669            .tunnel_options
2670            .iter_mut()
2671            .find(|t| t.outside_ip_address.as_deref() == Some(outside_ip))
2672            .ok_or_else(|| Ec2Error::InvalidVpnTunnelNotFound {
2673                vpn_connection_id: vpn_id.to_string(),
2674                outside_ip_address: outside_ip.to_string(),
2675            })?;
2676        tunnel.certificate_arn = Some(format!(
2677            "arn:aws:acm:us-east-1:000000000000:certificate/{}-{}",
2678            vpn_id, outside_ip
2679        ));
2680        Ok(self.vpn_connections.get(vpn_id).unwrap())
2681    }
2682
2683    pub fn modify_vpn_tunnel_options(
2684        &mut self,
2685        vpn_id: &str,
2686        outside_ip: &str,
2687        tunnel_inside_cidr: Option<String>,
2688        tunnel_inside_ipv6_cidr: Option<String>,
2689        pre_shared_key: Option<String>,
2690    ) -> Result<&VpnConnection, Ec2Error> {
2691        let vpn = self
2692            .vpn_connections
2693            .get_mut(vpn_id)
2694            .ok_or_else(|| Ec2Error::VpnConnectionNotFound(vpn_id.to_string()))?;
2695        let opts = vpn
2696            .options
2697            .get_or_insert_with(VpnConnectionOptions::default);
2698        let tunnel = opts
2699            .tunnel_options
2700            .iter_mut()
2701            .find(|t| t.outside_ip_address.as_deref() == Some(outside_ip))
2702            .ok_or_else(|| Ec2Error::InvalidVpnTunnelNotFound {
2703                vpn_connection_id: vpn_id.to_string(),
2704                outside_ip_address: outside_ip.to_string(),
2705            })?;
2706        if let Some(v) = tunnel_inside_cidr {
2707            tunnel.tunnel_inside_cidr = Some(v);
2708        }
2709        if let Some(v) = tunnel_inside_ipv6_cidr {
2710            tunnel.tunnel_inside_ipv6_cidr = Some(v);
2711        }
2712        if let Some(v) = pre_shared_key {
2713            tunnel.pre_shared_key = Some(v);
2714        }
2715        Ok(self.vpn_connections.get(vpn_id).unwrap())
2716    }
2717
2718    pub fn replace_vpn_tunnel(&mut self, vpn_id: &str, outside_ip: &str) -> Result<(), Ec2Error> {
2719        let vpn = self
2720            .vpn_connections
2721            .get_mut(vpn_id)
2722            .ok_or_else(|| Ec2Error::VpnConnectionNotFound(vpn_id.to_string()))?;
2723        // Verify the tunnel exists; we don't need to mutate it here.
2724        let exists = vpn
2725            .options
2726            .as_ref()
2727            .map(|o| {
2728                o.tunnel_options
2729                    .iter()
2730                    .any(|t| t.outside_ip_address.as_deref() == Some(outside_ip))
2731            })
2732            .unwrap_or(false);
2733        if !exists {
2734            return Err(Ec2Error::InvalidVpnTunnelNotFound {
2735                vpn_connection_id: vpn_id.to_string(),
2736                outside_ip_address: outside_ip.to_string(),
2737            });
2738        }
2739        vpn.tunnel_replacement_status = Some("pending".to_string());
2740        Ok(())
2741    }
2742
2743    // --- VPN concentrator operations ---
2744
2745    fn next_vpn_concentrator_id(&mut self) -> String {
2746        self.counters.vpn_concentrator += 1;
2747        format!("vpn-concentrator-{:08x}", self.counters.vpn_concentrator)
2748    }
2749
2750    pub fn create_vpn_concentrator(
2751        &mut self,
2752        concentrator_type: &str,
2753        transit_gateway_id: Option<String>,
2754        tags: Tags,
2755    ) -> &VpnConcentrator {
2756        let id = self.next_vpn_concentrator_id();
2757        // For TGW-attached concentrators, fabricate a deterministic attachment
2758        // id so describe round-trips. Pure mock; real AWS would create the
2759        // attachment as a side-effect of CreateVpnConcentrator.
2760        let attach_id = transit_gateway_id
2761            .as_ref()
2762            .map(|tgw| format!("tgw-attach-vpnc-{}-{tgw}", &id[16..]));
2763        let vc = VpnConcentrator {
2764            vpn_concentrator_id: id.clone(),
2765            concentrator_type: concentrator_type.to_string(),
2766            state: "available".to_string(),
2767            transit_gateway_id,
2768            transit_gateway_attachment_id: attach_id,
2769            tags,
2770        };
2771        self.vpn_concentrators.insert(id.clone(), vc);
2772        self.vpn_concentrators.get(&id).unwrap()
2773    }
2774
2775    pub fn delete_vpn_concentrator(&mut self, id: &str) -> Result<(), Ec2Error> {
2776        if self.vpn_concentrators.remove(id).is_none() {
2777            return Err(Ec2Error::InvalidVpnConcentratorNotFound(id.to_string()));
2778        }
2779        Ok(())
2780    }
2781
2782    // --- Carrier gateway operations ---
2783
2784    fn next_carrier_gw_id(&mut self) -> String {
2785        self.counters.cgw_carrier += 1;
2786        format!("cagw-{:08x}", self.counters.cgw_carrier)
2787    }
2788
2789    pub fn create_carrier_gateway(
2790        &mut self,
2791        vpc_id: &str,
2792        tags: Tags,
2793    ) -> Result<&CarrierGateway, Ec2Error> {
2794        if !self.vpcs.contains_key(vpc_id) {
2795            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
2796        }
2797        let id = self.next_carrier_gw_id();
2798        let cgw = CarrierGateway {
2799            carrier_gateway_id: id.clone(),
2800            vpc_id: vpc_id.to_string(),
2801            state: "available".to_string(),
2802            tags,
2803        };
2804        self.carrier_gateways.insert(id.clone(), cgw);
2805        Ok(self.carrier_gateways.get(&id).unwrap())
2806    }
2807
2808    pub fn delete_carrier_gateway(&mut self, id: &str) -> Result<(), Ec2Error> {
2809        let cgw = self
2810            .carrier_gateways
2811            .get_mut(id)
2812            .ok_or_else(|| Ec2Error::CarrierGatewayNotFound(id.to_string()))?;
2813        cgw.state = "deleted".to_string();
2814        Ok(())
2815    }
2816
2817    // --- Network interface operations ---
2818
2819    fn next_eni_id(&mut self) -> String {
2820        self.counters.eni += 1;
2821        format!("eni-{:08x}", self.counters.eni)
2822    }
2823
2824    fn next_eni_attach_id(&mut self) -> String {
2825        self.counters.eni_attach += 1;
2826        format!("eni-attach-{:08x}", self.counters.eni_attach)
2827    }
2828
2829    pub fn create_network_interface(
2830        &mut self,
2831        subnet_id: &str,
2832        description: &str,
2833        private_ip_address: &str,
2834        security_groups: Vec<String>,
2835        tags: Tags,
2836    ) -> Result<&NetworkInterface, Ec2Error> {
2837        let vpc_id = {
2838            let subnet = self
2839                .subnets
2840                .get(subnet_id)
2841                .ok_or_else(|| Ec2Error::SubnetNotFound(subnet_id.to_string()))?;
2842            subnet.vpc_id.clone()
2843        };
2844        let eni_id = self.next_eni_id();
2845        let eni = NetworkInterface {
2846            network_interface_id: eni_id.clone(),
2847            subnet_id: subnet_id.to_string(),
2848            vpc_id,
2849            description: description.to_string(),
2850            private_ip_address: if private_ip_address.is_empty() {
2851                "10.0.0.100".to_string()
2852            } else {
2853                private_ip_address.to_string()
2854            },
2855            status: "available".to_string(),
2856            attachment_id: None,
2857            instance_id: None,
2858            device_index: None,
2859            security_groups,
2860            source_dest_check: true,
2861            tags,
2862            public_ip_dns_hostname_type: None,
2863        };
2864        self.network_interfaces.insert(eni_id.clone(), eni);
2865        Ok(self.network_interfaces.get(&eni_id).unwrap())
2866    }
2867
2868    pub fn delete_network_interface(&mut self, eni_id: &str) -> Result<(), Ec2Error> {
2869        if self.network_interfaces.remove(eni_id).is_none() {
2870            return Err(Ec2Error::NetworkInterfaceNotFound(eni_id.to_string()));
2871        }
2872        Ok(())
2873    }
2874
2875    pub fn attach_network_interface(
2876        &mut self,
2877        eni_id: &str,
2878        instance_id: &str,
2879        device_index: i32,
2880    ) -> Result<String, Ec2Error> {
2881        let attach_id = self.next_eni_attach_id();
2882        let eni = self
2883            .network_interfaces
2884            .get_mut(eni_id)
2885            .ok_or_else(|| Ec2Error::NetworkInterfaceNotFound(eni_id.to_string()))?;
2886        eni.status = "in-use".to_string();
2887        eni.attachment_id = Some(attach_id.clone());
2888        eni.instance_id = Some(instance_id.to_string());
2889        eni.device_index = Some(device_index);
2890        Ok(attach_id)
2891    }
2892
2893    pub fn detach_network_interface(&mut self, attachment_id: &str) -> Result<(), Ec2Error> {
2894        for eni in self.network_interfaces.values_mut() {
2895            if eni.attachment_id.as_deref() == Some(attachment_id) {
2896                eni.status = "available".to_string();
2897                eni.attachment_id = None;
2898                eni.instance_id = None;
2899                eni.device_index = None;
2900                return Ok(());
2901            }
2902        }
2903        Err(Ec2Error::AttachmentNotFound(attachment_id.to_string()))
2904    }
2905
2906    // --- VPC CIDR block operations ---
2907
2908    fn next_cidr_assoc_id(&mut self) -> String {
2909        self.counters.vpc_cidr_assoc += 1;
2910        format!("vpc-cidr-assoc-{:08x}", self.counters.vpc_cidr_assoc)
2911    }
2912
2913    pub fn associate_vpc_cidr_block(
2914        &mut self,
2915        vpc_id: &str,
2916        cidr_block: &str,
2917    ) -> Result<String, Ec2Error> {
2918        if !self.vpcs.contains_key(vpc_id) {
2919            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
2920        }
2921        let assoc_id = self.next_cidr_assoc_id();
2922        let vpc = self.vpcs.get_mut(vpc_id).unwrap();
2923        vpc.secondary_cidr_blocks
2924            .push((assoc_id.clone(), cidr_block.to_string()));
2925        self.vpc_cidr_associations
2926            .insert(assoc_id.clone(), vpc_id.to_string());
2927        Ok(assoc_id)
2928    }
2929
2930    pub fn disassociate_vpc_cidr_block(&mut self, assoc_id: &str) -> Result<String, Ec2Error> {
2931        let vpc_id = self
2932            .vpc_cidr_associations
2933            .remove(assoc_id)
2934            .ok_or_else(|| Ec2Error::VpcCidrBlockAssociationNotFound(assoc_id.to_string()))?;
2935        if let Some(vpc) = self.vpcs.get_mut(&vpc_id) {
2936            vpc.secondary_cidr_blocks.retain(|(id, _)| id != assoc_id);
2937        }
2938        Ok(vpc_id)
2939    }
2940
2941    // --- Security group rule operations ---
2942
2943    pub fn describe_security_group_rules(
2944        &self,
2945        group_id: Option<&str>,
2946        rule_ids: &[String],
2947    ) -> Vec<(&str, bool, &IpPermission)> {
2948        // Returns (group_id, is_egress, rule)
2949        let mut results = Vec::new();
2950        for sg in self.security_groups.values() {
2951            let matches_group = group_id.is_none_or(|gid| sg.group_id == gid);
2952            if !matches_group {
2953                continue;
2954            }
2955            for rule in &sg.ingress_rules {
2956                if rule_ids.is_empty() || rule_ids.contains(&rule.rule_id) {
2957                    results.push((sg.group_id.as_str(), false, rule));
2958                }
2959            }
2960            for rule in &sg.egress_rules {
2961                if rule_ids.is_empty() || rule_ids.contains(&rule.rule_id) {
2962                    results.push((sg.group_id.as_str(), true, rule));
2963                }
2964            }
2965        }
2966        results
2967    }
2968
2969    pub fn update_sgr_description_ingress(
2970        &mut self,
2971        group_id: &str,
2972        rule_id: &str,
2973        description: Option<String>,
2974    ) -> Result<(), Ec2Error> {
2975        let sg = self
2976            .security_groups
2977            .get_mut(group_id)
2978            .ok_or_else(|| Ec2Error::SecurityGroupNotFound(group_id.to_string()))?;
2979        for rule in sg.ingress_rules.iter_mut() {
2980            if rule.rule_id == rule_id {
2981                for range in rule.ip_ranges.iter_mut() {
2982                    range.description = description.clone();
2983                }
2984                return Ok(());
2985            }
2986        }
2987        Ok(())
2988    }
2989
2990    pub fn update_sgr_description_egress(
2991        &mut self,
2992        group_id: &str,
2993        rule_id: &str,
2994        description: Option<String>,
2995    ) -> Result<(), Ec2Error> {
2996        let sg = self
2997            .security_groups
2998            .get_mut(group_id)
2999            .ok_or_else(|| Ec2Error::SecurityGroupNotFound(group_id.to_string()))?;
3000        for rule in sg.egress_rules.iter_mut() {
3001            if rule.rule_id == rule_id {
3002                for range in rule.ip_ranges.iter_mut() {
3003                    range.description = description.clone();
3004                }
3005                return Ok(());
3006            }
3007        }
3008        Ok(())
3009    }
3010
3011    // --- Instance operations ---
3012
3013    pub fn run_instances(
3014        &mut self,
3015        image_id: &str,
3016        instance_type: &str,
3017        min_count: i32,
3018        max_count: i32,
3019        key_name: Option<String>,
3020        subnet_id: Option<String>,
3021        security_group_ids: Vec<String>,
3022        tags: Tags,
3023        iam_instance_profile_arn: Option<String>,
3024        placement_az: &str,
3025        owner_id: &str,
3026    ) -> Vec<Instance> {
3027        let count = max_count.max(min_count).max(1) as usize;
3028        let now = chrono::Utc::now()
3029            .format("%Y-%m-%dT%H:%M:%S.000Z")
3030            .to_string();
3031        let vpc_id = subnet_id
3032            .as_deref()
3033            .and_then(|sid| self.subnets.get(sid).map(|s| s.vpc_id.clone()));
3034        let mut instances = Vec::new();
3035        for _ in 0..count {
3036            let instance_id = self.next_instance_id();
3037            let private_ip = Some(format!(
3038                "10.0.{}.{}",
3039                (self.counters.instance >> 8) & 0xff,
3040                self.counters.instance & 0xff
3041            ));
3042            let instance = Instance {
3043                instance_id: instance_id.clone(),
3044                image_id: image_id.to_string(),
3045                instance_type: instance_type.to_string(),
3046                state: InstanceState {
3047                    code: 16,
3048                    name: "running".to_string(),
3049                },
3050                private_ip_address: private_ip,
3051                public_ip_address: None,
3052                subnet_id: subnet_id.clone(),
3053                vpc_id: vpc_id.clone(),
3054                key_name: key_name.clone(),
3055                security_groups: security_group_ids.clone(),
3056                launch_time: now.clone(),
3057                tags: tags.clone(),
3058                iam_instance_profile_arn: iam_instance_profile_arn.clone(),
3059                monitoring_state: "disabled".to_string(),
3060                placement_az: placement_az.to_string(),
3061                placement_group_name: None,
3062                placement_tenancy: None,
3063                placement_host_id: None,
3064                placement_affinity: None,
3065                placement_partition_number: None,
3066                owner_id: owner_id.to_string(),
3067                classic_link_vpc: None,
3068                private_dns_hostname_type: None,
3069                enable_resource_name_dns_a_record: None,
3070                enable_resource_name_dns_aaaa_record: None,
3071                credit_specification: None,
3072                cpu_options: None,
3073                maintenance_options: None,
3074                network_bandwidth_weighting: None,
3075                lifecycle: None,
3076                product_codes: Vec::new(),
3077                capacity_reservation_specification: None,
3078            };
3079            self.instances.insert(instance_id, instance.clone());
3080            instances.push(instance);
3081        }
3082        instances
3083    }
3084
3085    pub fn start_instances(
3086        &mut self,
3087        instance_ids: &[String],
3088    ) -> Vec<(String, i32, String, i32, String)> {
3089        // Returns (id, prev_code, prev_name, curr_code, curr_name)
3090        let mut changes = Vec::new();
3091        for id in instance_ids {
3092            if let Some(inst) = self.instances.get_mut(id) {
3093                let prev_code = inst.state.code;
3094                let prev_name = inst.state.name.clone();
3095                inst.state = InstanceState {
3096                    code: 16,
3097                    name: "running".to_string(),
3098                };
3099                changes.push((id.clone(), prev_code, prev_name, 16, "running".to_string()));
3100            }
3101        }
3102        changes
3103    }
3104
3105    pub fn stop_instances(
3106        &mut self,
3107        instance_ids: &[String],
3108    ) -> Vec<(String, i32, String, i32, String)> {
3109        let mut changes = Vec::new();
3110        for id in instance_ids {
3111            if let Some(inst) = self.instances.get_mut(id) {
3112                let prev_code = inst.state.code;
3113                let prev_name = inst.state.name.clone();
3114                inst.state = InstanceState {
3115                    code: 80,
3116                    name: "stopped".to_string(),
3117                };
3118                changes.push((id.clone(), prev_code, prev_name, 80, "stopped".to_string()));
3119            }
3120        }
3121        changes
3122    }
3123
3124    pub fn terminate_instances(
3125        &mut self,
3126        instance_ids: &[String],
3127    ) -> Vec<(String, i32, String, i32, String)> {
3128        let mut changes = Vec::new();
3129        for id in instance_ids {
3130            if let Some(inst) = self.instances.get_mut(id) {
3131                let prev_code = inst.state.code;
3132                let prev_name = inst.state.name.clone();
3133                inst.state = InstanceState {
3134                    code: 48,
3135                    name: "terminated".to_string(),
3136                };
3137                changes.push((
3138                    id.clone(),
3139                    prev_code,
3140                    prev_name,
3141                    48,
3142                    "terminated".to_string(),
3143                ));
3144            }
3145        }
3146        changes
3147    }
3148
3149    // --- EBS Volume operations ---
3150
3151    pub fn create_volume(
3152        &mut self,
3153        size: i32,
3154        availability_zone: &str,
3155        snapshot_id: Option<String>,
3156        volume_type: &str,
3157        iops: Option<i32>,
3158        throughput: Option<i32>,
3159        encrypted: bool,
3160        tags: Tags,
3161    ) -> &Volume {
3162        let vol_id = self.next_vol_id();
3163        let now = chrono::Utc::now()
3164            .format("%Y-%m-%dT%H:%M:%S.000Z")
3165            .to_string();
3166        let vol = Volume {
3167            volume_id: vol_id.clone(),
3168            size,
3169            snapshot_id,
3170            availability_zone: availability_zone.to_string(),
3171            state: "available".to_string(),
3172            volume_type: volume_type.to_string(),
3173            iops,
3174            throughput,
3175            encrypted,
3176            create_time: now,
3177            attachments: Vec::new(),
3178            tags,
3179            recycle_bin_state: None,
3180            source_volume_id: None,
3181        };
3182        self.volumes.insert(vol_id.clone(), vol);
3183        self.volumes.get(&vol_id).unwrap()
3184    }
3185
3186    pub fn delete_volume(&mut self, vol_id: &str) -> Result<(), Ec2Error> {
3187        // Mock policy: every deleted volume is moved into the Recycle Bin so
3188        // it can be restored via `RestoreVolumeFromRecycleBin`. The volume is
3189        // removed from `self.volumes` (so `DescribeVolumes` does not surface
3190        // it) and parked in `self.deleted_volumes_recycle_bin` keyed by ID.
3191        let mut vol = self
3192            .volumes
3193            .remove(vol_id)
3194            .ok_or_else(|| Ec2Error::VolumeNotFound(vol_id.to_string()))?;
3195        vol.state = "deleted-pending-recycle".to_string();
3196        vol.recycle_bin_state = Some("in-recycle-bin".to_string());
3197        self.deleted_volumes_recycle_bin
3198            .insert(vol_id.to_string(), vol);
3199        Ok(())
3200    }
3201
3202    pub fn restore_volume_from_recycle_bin(&mut self, vol_id: &str) -> Result<(), Ec2Error> {
3203        let mut vol = self
3204            .deleted_volumes_recycle_bin
3205            .remove(vol_id)
3206            .ok_or_else(|| Ec2Error::VolumeNotInRecycleBin(vol_id.to_string()))?;
3207        vol.state = "available".to_string();
3208        vol.recycle_bin_state = None;
3209        self.volumes.insert(vol_id.to_string(), vol);
3210        Ok(())
3211    }
3212
3213    pub fn copy_volumes(
3214        &mut self,
3215        source_volume_ids: &[String],
3216        tags: Tags,
3217    ) -> Result<Vec<String>, Ec2Error> {
3218        // Verify all sources exist first.
3219        for src in source_volume_ids {
3220            if !self.volumes.contains_key(src) {
3221                return Err(Ec2Error::VolumeNotFound(src.clone()));
3222            }
3223        }
3224        let mut new_ids = Vec::new();
3225        for src in source_volume_ids {
3226            // Snapshot the source's properties first so we can release the
3227            // immutable borrow before mutating the map.
3228            let (size, az, vol_type, iops, throughput, encrypted, snapshot_id) = {
3229                let v = self.volumes.get(src).unwrap();
3230                (
3231                    v.size,
3232                    v.availability_zone.clone(),
3233                    v.volume_type.clone(),
3234                    v.iops,
3235                    v.throughput,
3236                    v.encrypted,
3237                    v.snapshot_id.clone(),
3238                )
3239            };
3240            let new_id = self.next_vol_id();
3241            let now = chrono::Utc::now()
3242                .format("%Y-%m-%dT%H:%M:%S.000Z")
3243                .to_string();
3244            let new_vol = Volume {
3245                volume_id: new_id.clone(),
3246                size,
3247                snapshot_id,
3248                availability_zone: az,
3249                state: "available".to_string(),
3250                volume_type: vol_type,
3251                iops,
3252                throughput,
3253                encrypted,
3254                create_time: now,
3255                attachments: Vec::new(),
3256                tags: tags.clone(),
3257                recycle_bin_state: None,
3258                source_volume_id: Some(src.clone()),
3259            };
3260            self.volumes.insert(new_id.clone(), new_vol);
3261            new_ids.push(new_id);
3262        }
3263        Ok(new_ids)
3264    }
3265
3266    pub fn attach_volume(
3267        &mut self,
3268        vol_id: &str,
3269        instance_id: &str,
3270        device: &str,
3271    ) -> Result<&VolumeAttachment, Ec2Error> {
3272        if !self.instances.contains_key(instance_id) {
3273            return Err(Ec2Error::InstanceNotFound(instance_id.to_string()));
3274        }
3275        let now = chrono::Utc::now()
3276            .format("%Y-%m-%dT%H:%M:%S.000Z")
3277            .to_string();
3278        let vol = self
3279            .volumes
3280            .get_mut(vol_id)
3281            .ok_or_else(|| Ec2Error::VolumeNotFound(vol_id.to_string()))?;
3282        vol.state = "in-use".to_string();
3283        vol.attachments.push(VolumeAttachment {
3284            volume_id: vol_id.to_string(),
3285            instance_id: instance_id.to_string(),
3286            device: device.to_string(),
3287            state: "attached".to_string(),
3288            attach_time: now,
3289            delete_on_termination: false,
3290        });
3291        Ok(vol.attachments.last().unwrap())
3292    }
3293
3294    pub fn detach_volume(
3295        &mut self,
3296        vol_id: &str,
3297        instance_id: Option<&str>,
3298        device: Option<&str>,
3299    ) -> Result<VolumeAttachment, Ec2Error> {
3300        let vol = self
3301            .volumes
3302            .get_mut(vol_id)
3303            .ok_or_else(|| Ec2Error::VolumeNotFound(vol_id.to_string()))?;
3304        let pos = vol
3305            .attachments
3306            .iter()
3307            .position(|a| {
3308                instance_id.is_none_or(|id| a.instance_id == id)
3309                    && device.is_none_or(|d| a.device == d)
3310            })
3311            .ok_or(Ec2Error::VolumeNotAttached)?;
3312        let att = vol.attachments.remove(pos);
3313        if vol.attachments.is_empty() {
3314            vol.state = "available".to_string();
3315        }
3316        Ok(att)
3317    }
3318
3319    // --- Snapshot operations ---
3320
3321    pub fn create_snapshot(
3322        &mut self,
3323        vol_id: &str,
3324        description: &str,
3325        tags: Tags,
3326        owner_id: &str,
3327    ) -> Result<&Snapshot, Ec2Error> {
3328        let vol = self
3329            .volumes
3330            .get(vol_id)
3331            .ok_or_else(|| Ec2Error::VolumeNotFound(vol_id.to_string()))?;
3332        let vol_size = vol.size;
3333        let encrypted = vol.encrypted;
3334        let snap_id = self.next_snapshot_id();
3335        let now = chrono::Utc::now()
3336            .format("%Y-%m-%dT%H:%M:%S.000Z")
3337            .to_string();
3338        let snap = Snapshot {
3339            snapshot_id: snap_id.clone(),
3340            volume_id: vol_id.to_string(),
3341            volume_size: vol_size,
3342            state: "completed".to_string(),
3343            description: description.to_string(),
3344            start_time: now,
3345            progress: "100%".to_string(),
3346            owner_id: owner_id.to_string(),
3347            encrypted,
3348            tags,
3349            lock_state: "none".to_string(),
3350            lock_duration: None,
3351            lock_created_on: None,
3352            lock_expires_on: None,
3353            lock_duration_start_time: None,
3354            cool_off_period: None,
3355            cool_off_period_expires_on: None,
3356            storage_tier: "standard".to_string(),
3357            last_tiering_operation_status: None,
3358            fast_snapshot_restore_states: Vec::new(),
3359        };
3360        self.snapshots.insert(snap_id.clone(), snap);
3361        Ok(self.snapshots.get(&snap_id).unwrap())
3362    }
3363
3364    pub fn delete_snapshot(&mut self, snap_id: &str) -> Result<(), Ec2Error> {
3365        // Mock policy: deletion sends the snapshot to the recycle bin where it
3366        // can be restored via `RestoreSnapshotFromRecycleBin`.
3367        let snap = self
3368            .snapshots
3369            .get(snap_id)
3370            .ok_or_else(|| Ec2Error::SnapshotNotFound(snap_id.to_string()))?;
3371        if snap.lock_state == "compliance" || snap.lock_state == "governance" {
3372            return Err(Ec2Error::SnapshotIsLocked(snap_id.to_string()));
3373        }
3374        let mut snap = self.snapshots.remove(snap_id).unwrap();
3375        snap.state = "recycled".to_string();
3376        self.deleted_snapshots_recycle_bin
3377            .insert(snap_id.to_string(), snap);
3378        Ok(())
3379    }
3380
3381    pub fn restore_snapshot_from_recycle_bin(
3382        &mut self,
3383        snap_id: &str,
3384    ) -> Result<&Snapshot, Ec2Error> {
3385        let mut snap = self
3386            .deleted_snapshots_recycle_bin
3387            .remove(snap_id)
3388            .ok_or_else(|| Ec2Error::SnapshotNotInRecycleBin(snap_id.to_string()))?;
3389        snap.state = "completed".to_string();
3390        self.snapshots.insert(snap_id.to_string(), snap);
3391        Ok(self.snapshots.get(snap_id).unwrap())
3392    }
3393
3394    pub fn lock_snapshot(
3395        &mut self,
3396        snap_id: &str,
3397        lock_mode: &str,
3398        lock_duration: Option<i32>,
3399        cool_off_period: Option<i32>,
3400    ) -> Result<&Snapshot, Ec2Error> {
3401        let snap = self
3402            .snapshots
3403            .get_mut(snap_id)
3404            .ok_or_else(|| Ec2Error::SnapshotNotFound(snap_id.to_string()))?;
3405        // Compliance-locked snapshots cannot be re-locked or modified.
3406        if snap.lock_state == "compliance" {
3407            return Err(Ec2Error::SnapshotIsLocked(snap_id.to_string()));
3408        }
3409        let now = chrono::Utc::now()
3410            .format("%Y-%m-%dT%H:%M:%S.000Z")
3411            .to_string();
3412        snap.lock_state = lock_mode.to_string();
3413        snap.lock_created_on = Some(now.clone());
3414        snap.lock_duration_start_time = Some(now.clone());
3415        snap.lock_duration = lock_duration;
3416        snap.lock_expires_on = lock_duration.map(|d| {
3417            let expires = chrono::Utc::now() + chrono::Duration::days(d as i64);
3418            expires.format("%Y-%m-%dT%H:%M:%S.000Z").to_string()
3419        });
3420        snap.cool_off_period = cool_off_period;
3421        snap.cool_off_period_expires_on = cool_off_period.map(|cp| {
3422            let expires = chrono::Utc::now() + chrono::Duration::hours(cp as i64);
3423            expires.format("%Y-%m-%dT%H:%M:%S.000Z").to_string()
3424        });
3425        Ok(self.snapshots.get(snap_id).unwrap())
3426    }
3427
3428    pub fn unlock_snapshot(&mut self, snap_id: &str) -> Result<(), Ec2Error> {
3429        let snap = self
3430            .snapshots
3431            .get_mut(snap_id)
3432            .ok_or_else(|| Ec2Error::SnapshotNotFound(snap_id.to_string()))?;
3433        if snap.lock_state == "compliance" {
3434            return Err(Ec2Error::SnapshotIsLocked(snap_id.to_string()));
3435        }
3436        snap.lock_state = "none".to_string();
3437        snap.lock_duration = None;
3438        snap.lock_created_on = None;
3439        snap.lock_expires_on = None;
3440        snap.lock_duration_start_time = None;
3441        snap.cool_off_period = None;
3442        snap.cool_off_period_expires_on = None;
3443        Ok(())
3444    }
3445
3446    pub fn modify_snapshot_tier(
3447        &mut self,
3448        snap_id: &str,
3449        storage_tier: &str,
3450    ) -> Result<String, Ec2Error> {
3451        let snap = self
3452            .snapshots
3453            .get_mut(snap_id)
3454            .ok_or_else(|| Ec2Error::SnapshotNotFound(snap_id.to_string()))?;
3455        snap.storage_tier = storage_tier.to_string();
3456        snap.last_tiering_operation_status = Some(format!("tiering-in-progress-to-{storage_tier}"));
3457        let now = chrono::Utc::now()
3458            .format("%Y-%m-%dT%H:%M:%S.000Z")
3459            .to_string();
3460        Ok(now)
3461    }
3462
3463    pub fn restore_snapshot_tier(&mut self, snap_id: &str) -> Result<String, Ec2Error> {
3464        let snap = self
3465            .snapshots
3466            .get_mut(snap_id)
3467            .ok_or_else(|| Ec2Error::SnapshotNotFound(snap_id.to_string()))?;
3468        snap.storage_tier = "standard".to_string();
3469        snap.last_tiering_operation_status = Some("tiering-finished".to_string());
3470        let now = chrono::Utc::now()
3471            .format("%Y-%m-%dT%H:%M:%S.000Z")
3472            .to_string();
3473        Ok(now)
3474    }
3475
3476    // --- AMI / Image operations ---
3477
3478    pub fn create_image(
3479        &mut self,
3480        instance_id: &str,
3481        name: &str,
3482        description: &str,
3483        tags: Tags,
3484        owner_id: &str,
3485    ) -> Result<String, Ec2Error> {
3486        if !self.instances.contains_key(instance_id) {
3487            return Err(Ec2Error::InstanceNotFound(instance_id.to_string()));
3488        }
3489        let source_instance_type = self
3490            .instances
3491            .get(instance_id)
3492            .unwrap()
3493            .instance_type
3494            .clone();
3495        let image_id = self.next_ami_id();
3496        let img = Image {
3497            image_id: image_id.clone(),
3498            name: name.to_string(),
3499            description: description.to_string(),
3500            state: "available".to_string(),
3501            owner_id: owner_id.to_string(),
3502            architecture: "x86_64".to_string(),
3503            image_type: "machine".to_string(),
3504            platform: None,
3505            virtualization_type: "hvm".to_string(),
3506            root_device_type: "ebs".to_string(),
3507            root_device_name: "/dev/xvda".to_string(),
3508            public: false,
3509            tags,
3510            source_instance_id: Some(instance_id.to_string()),
3511            source_instance_type,
3512            launch_permissions: Vec::new(),
3513            recycle_bin_state: None,
3514            deprecation_time: None,
3515            recycle_bin_enter_time: None,
3516            product_codes: Vec::new(),
3517            fast_launch_state: None,
3518            deregistration_protection: None,
3519            kernel_id: None,
3520            ramdisk_id: None,
3521            ena_support: None,
3522            sriov_net_support: None,
3523            tpm_support: None,
3524            boot_mode: None,
3525            imds_support: None,
3526            image_location: None,
3527            source_image_id: None,
3528            source_region: None,
3529        };
3530        self.images.insert(image_id.clone(), img);
3531        Ok(image_id)
3532    }
3533
3534    pub fn modify_image_launch_permissions(
3535        &mut self,
3536        image_id: &str,
3537        add: &[crate::types::LaunchPermission],
3538        remove: &[crate::types::LaunchPermission],
3539    ) -> Result<(), Ec2Error> {
3540        let img = self
3541            .images
3542            .get_mut(image_id)
3543            .ok_or_else(|| Ec2Error::AmiNotFound(image_id.to_string()))?;
3544        for perm in remove {
3545            img.launch_permissions.retain(|p| p != perm);
3546        }
3547        for perm in add {
3548            if !img.launch_permissions.contains(perm) {
3549                img.launch_permissions.push(perm.clone());
3550            }
3551        }
3552        Ok(())
3553    }
3554
3555    pub fn modify_image_description(
3556        &mut self,
3557        image_id: &str,
3558        description: &str,
3559    ) -> Result<(), Ec2Error> {
3560        let img = self
3561            .images
3562            .get_mut(image_id)
3563            .ok_or_else(|| Ec2Error::AmiNotFound(image_id.to_string()))?;
3564        img.description = description.to_string();
3565        Ok(())
3566    }
3567
3568    pub fn get_image(&self, image_id: &str) -> Result<&crate::types::Image, Ec2Error> {
3569        self.images
3570            .get(image_id)
3571            .ok_or_else(|| Ec2Error::AmiNotFound(image_id.to_string()))
3572    }
3573
3574    pub fn deregister_image(&mut self, image_id: &str) -> Result<(), Ec2Error> {
3575        // Move the image to the recycle bin instead of fully deleting it,
3576        // so RestoreImageFromRecycleBin can recover it.
3577        let img = self
3578            .images
3579            .get_mut(image_id)
3580            .ok_or_else(|| Ec2Error::AmiNotFound(image_id.to_string()))?;
3581        img.recycle_bin_state = Some("recycled".to_string());
3582        img.state = "deregistered".to_string();
3583        self.deregistered_images.insert(image_id.to_string());
3584        Ok(())
3585    }
3586
3587    // --- Launch Template operations ---
3588
3589    pub fn create_launch_template(
3590        &mut self,
3591        name: &str,
3592        version_description: &str,
3593        data: std::collections::HashMap<String, String>,
3594        tags: Tags,
3595    ) -> Result<&LaunchTemplate, Ec2Error> {
3596        // Check for duplicate name
3597        if self
3598            .launch_templates
3599            .values()
3600            .any(|lt| lt.launch_template_name == name)
3601        {
3602            return Err(Ec2Error::LaunchTemplateAlreadyExists(name.to_string()));
3603        }
3604        let lt_id = self.next_lt_id();
3605        let lt = LaunchTemplate {
3606            launch_template_id: lt_id.clone(),
3607            launch_template_name: name.to_string(),
3608            default_version_number: 1,
3609            latest_version_number: 1,
3610            tags: tags.clone(),
3611        };
3612        self.launch_templates.insert(lt_id.clone(), lt);
3613        let version = LaunchTemplateVersion {
3614            version_number: 1,
3615            launch_template_id: lt_id.clone(),
3616            launch_template_name: name.to_string(),
3617            version_description: version_description.to_string(),
3618            data,
3619            default_version: true,
3620        };
3621        self.launch_template_versions
3622            .entry(lt_id.clone())
3623            .or_default()
3624            .push(version);
3625        Ok(self.launch_templates.get(&lt_id).unwrap())
3626    }
3627
3628    pub fn delete_launch_template(
3629        &mut self,
3630        lt_id_or_name: &str,
3631    ) -> Result<LaunchTemplate, Ec2Error> {
3632        // Try by ID first, then by name
3633        let lt_id = if self.launch_templates.contains_key(lt_id_or_name) {
3634            lt_id_or_name.to_string()
3635        } else {
3636            self.launch_templates
3637                .values()
3638                .find(|lt| lt.launch_template_name == lt_id_or_name)
3639                .map(|lt| lt.launch_template_id.clone())
3640                .ok_or_else(|| Ec2Error::LaunchTemplateNotFound(lt_id_or_name.to_string()))?
3641        };
3642        self.launch_template_versions.remove(&lt_id);
3643        Ok(self.launch_templates.remove(&lt_id).unwrap())
3644    }
3645
3646    // --- Launch Template Version operations ---
3647
3648    pub fn create_launch_template_version(
3649        &mut self,
3650        lt_id: &str,
3651        description: &str,
3652        data: HashMap<String, String>,
3653    ) -> Result<i64, Ec2Error> {
3654        // Also accept by name
3655        let lt_id_resolved = if self.launch_templates.contains_key(lt_id) {
3656            lt_id.to_string()
3657        } else {
3658            self.launch_templates
3659                .values()
3660                .find(|lt| lt.launch_template_name == lt_id)
3661                .map(|lt| lt.launch_template_id.clone())
3662                .ok_or_else(|| Ec2Error::LaunchTemplateNotFound(lt_id.to_string()))?
3663        };
3664        let lt = self.launch_templates.get_mut(&lt_id_resolved).unwrap();
3665        lt.latest_version_number += 1;
3666        let version_number = lt.latest_version_number;
3667        let lt_name = lt.launch_template_name.clone();
3668        let version = LaunchTemplateVersion {
3669            version_number,
3670            launch_template_id: lt_id_resolved.clone(),
3671            launch_template_name: lt_name,
3672            version_description: description.to_string(),
3673            data,
3674            default_version: false,
3675        };
3676        self.launch_template_versions
3677            .entry(lt_id_resolved)
3678            .or_default()
3679            .push(version);
3680        Ok(version_number)
3681    }
3682
3683    pub fn modify_launch_template(
3684        &mut self,
3685        lt_id: &str,
3686        default_version: Option<i64>,
3687    ) -> Result<(), Ec2Error> {
3688        let lt = self
3689            .launch_templates
3690            .get_mut(lt_id)
3691            .ok_or_else(|| Ec2Error::LaunchTemplateNotFound(lt_id.to_string()))?;
3692        if let Some(v) = default_version {
3693            lt.default_version_number = v;
3694            if let Some(versions) = self.launch_template_versions.get_mut(lt_id) {
3695                for ver in versions.iter_mut() {
3696                    ver.default_version = ver.version_number == v;
3697                }
3698            }
3699        }
3700        Ok(())
3701    }
3702
3703    pub fn get_launch_template_data_from_instance(
3704        &self,
3705        instance_id: &str,
3706    ) -> HashMap<String, String> {
3707        let mut data = HashMap::new();
3708        if let Some(inst) = self.instances.get(instance_id) {
3709            data.insert(
3710                "LaunchTemplateData.ImageId".to_string(),
3711                inst.image_id.clone(),
3712            );
3713            data.insert(
3714                "LaunchTemplateData.InstanceType".to_string(),
3715                inst.instance_type.clone(),
3716            );
3717            if let Some(key) = &inst.key_name {
3718                data.insert("LaunchTemplateData.KeyName".to_string(), key.clone());
3719            }
3720        }
3721        data
3722    }
3723
3724    // --- Transit Gateway operations ---
3725
3726    pub fn create_transit_gateway(
3727        &mut self,
3728        description: &str,
3729        amazon_side_asn: i64,
3730        dns_support: &str,
3731        vpn_ecmp_support: &str,
3732        tags: Tags,
3733    ) -> &TransitGateway {
3734        let tgw_id = self.next_tgw_id();
3735        let tgw = TransitGateway {
3736            transit_gateway_id: tgw_id.clone(),
3737            state: "available".to_string(),
3738            amazon_side_asn,
3739            description: description.to_string(),
3740            dns_support: dns_support.to_string(),
3741            vpn_ecmp_support: vpn_ecmp_support.to_string(),
3742            multicast_support: "disable".to_string(),
3743            tags,
3744        };
3745        self.transit_gateways.insert(tgw_id.clone(), tgw);
3746        self.transit_gateways.get(&tgw_id).unwrap()
3747    }
3748
3749    pub fn delete_transit_gateway(&mut self, tgw_id: &str) -> Result<(), Ec2Error> {
3750        let tgw = self
3751            .transit_gateways
3752            .get_mut(tgw_id)
3753            .ok_or_else(|| Ec2Error::TransitGatewayNotFound(tgw_id.to_string()))?;
3754        tgw.state = "deleted".to_string();
3755        Ok(())
3756    }
3757
3758    pub fn create_transit_gateway_vpc_attachment(
3759        &mut self,
3760        tgw_id: &str,
3761        vpc_id: &str,
3762        subnet_ids: Vec<String>,
3763        tags: Tags,
3764    ) -> Result<&TransitGatewayVpcAttachment, Ec2Error> {
3765        if !self.transit_gateways.contains_key(tgw_id) {
3766            return Err(Ec2Error::TransitGatewayNotFound(tgw_id.to_string()));
3767        }
3768        let attach_id = self.next_tgw_attach_id();
3769        let att = TransitGatewayVpcAttachment {
3770            attachment_id: attach_id.clone(),
3771            transit_gateway_id: tgw_id.to_string(),
3772            vpc_id: vpc_id.to_string(),
3773            subnet_ids,
3774            state: "available".to_string(),
3775            tags,
3776        };
3777        self.tgw_vpc_attachments.insert(attach_id.clone(), att);
3778        Ok(self.tgw_vpc_attachments.get(&attach_id).unwrap())
3779    }
3780
3781    pub fn delete_transit_gateway_vpc_attachment(
3782        &mut self,
3783        attach_id: &str,
3784    ) -> Result<(), Ec2Error> {
3785        let att = self
3786            .tgw_vpc_attachments
3787            .get_mut(attach_id)
3788            .ok_or_else(|| Ec2Error::TgwVpcAttachmentNotFound(attach_id.to_string()))?;
3789        att.state = "deleted".to_string();
3790        Ok(())
3791    }
3792
3793    pub fn create_transit_gateway_route_table(
3794        &mut self,
3795        tgw_id: &str,
3796        tags: Tags,
3797    ) -> Result<&TransitGatewayRouteTable, Ec2Error> {
3798        if !self.transit_gateways.contains_key(tgw_id) {
3799            return Err(Ec2Error::TransitGatewayNotFound(tgw_id.to_string()));
3800        }
3801        let rtb_id = self.next_tgw_rtb_id();
3802        let rtb = TransitGatewayRouteTable {
3803            route_table_id: rtb_id.clone(),
3804            transit_gateway_id: tgw_id.to_string(),
3805            state: "available".to_string(),
3806            default_association_route_table: false,
3807            default_propagation_route_table: false,
3808            tags,
3809        };
3810        self.tgw_route_tables.insert(rtb_id.clone(), rtb);
3811        Ok(self.tgw_route_tables.get(&rtb_id).unwrap())
3812    }
3813
3814    pub fn delete_transit_gateway_route_table(&mut self, rtb_id: &str) -> Result<(), Ec2Error> {
3815        let rtb = self
3816            .tgw_route_tables
3817            .get_mut(rtb_id)
3818            .ok_or_else(|| Ec2Error::TgwRouteTableNotFound(rtb_id.to_string()))?;
3819        rtb.state = "deleted".to_string();
3820        Ok(())
3821    }
3822
3823    pub fn create_transit_gateway_route(
3824        &mut self,
3825        rtb_id: &str,
3826        cidr: &str,
3827        attachment_id: Option<String>,
3828    ) -> Result<(), Ec2Error> {
3829        if !self.tgw_route_tables.contains_key(rtb_id) {
3830            return Err(Ec2Error::TgwRouteTableNotFound(rtb_id.to_string()));
3831        }
3832        let route = TransitGatewayRoute {
3833            destination_cidr_block: cidr.to_string(),
3834            route_type: "static".to_string(),
3835            state: "active".to_string(),
3836            attachment_id,
3837        };
3838        self.tgw_routes
3839            .entry(rtb_id.to_string())
3840            .or_default()
3841            .push(route);
3842        Ok(())
3843    }
3844
3845    pub fn delete_transit_gateway_route(
3846        &mut self,
3847        rtb_id: &str,
3848        cidr: &str,
3849    ) -> Result<(), Ec2Error> {
3850        let routes = self
3851            .tgw_routes
3852            .get_mut(rtb_id)
3853            .ok_or_else(|| Ec2Error::TgwRouteTableNotFound(rtb_id.to_string()))?;
3854        routes.retain(|r| r.destination_cidr_block != cidr);
3855        Ok(())
3856    }
3857
3858    pub fn create_transit_gateway_peering_attachment(
3859        &mut self,
3860        tgw_id: &str,
3861        peer_tgw_id: &str,
3862    ) -> &TransitGatewayPeeringAttachment {
3863        let attach_id = self.next_tgw_attach_id();
3864        let att = TransitGatewayPeeringAttachment {
3865            attachment_id: attach_id.clone(),
3866            transit_gateway_id: tgw_id.to_string(),
3867            peer_transit_gateway_id: peer_tgw_id.to_string(),
3868            peer_account_id: "123456789012".to_string(),
3869            peer_region: "us-east-1".to_string(),
3870            state: "pendingAcceptance".to_string(),
3871            tags: HashMap::new(),
3872        };
3873        self.tgw_peering_attachments.insert(attach_id.clone(), att);
3874        self.tgw_peering_attachments.get(&attach_id).unwrap()
3875    }
3876
3877    pub fn accept_transit_gateway_peering_attachment(
3878        &mut self,
3879        attach_id: &str,
3880    ) -> Result<(), Ec2Error> {
3881        let att = self
3882            .tgw_peering_attachments
3883            .get_mut(attach_id)
3884            .ok_or_else(|| Ec2Error::TgwPeeringAttachmentNotFound(attach_id.to_string()))?;
3885        att.state = "available".to_string();
3886        Ok(())
3887    }
3888
3889    pub fn reject_transit_gateway_peering_attachment(
3890        &mut self,
3891        attach_id: &str,
3892    ) -> Result<(), Ec2Error> {
3893        let att = self
3894            .tgw_peering_attachments
3895            .get_mut(attach_id)
3896            .ok_or_else(|| Ec2Error::TgwPeeringAttachmentNotFound(attach_id.to_string()))?;
3897        att.state = "rejected".to_string();
3898        Ok(())
3899    }
3900
3901    pub fn delete_transit_gateway_peering_attachment(
3902        &mut self,
3903        attach_id: &str,
3904    ) -> Result<(), Ec2Error> {
3905        let att = self
3906            .tgw_peering_attachments
3907            .get_mut(attach_id)
3908            .ok_or_else(|| Ec2Error::TgwPeeringAttachmentNotFound(attach_id.to_string()))?;
3909        att.state = "deleted".to_string();
3910        Ok(())
3911    }
3912
3913    // --- Spot Instance operations ---
3914
3915    pub fn request_spot_instances(
3916        &mut self,
3917        spot_price: &str,
3918        count: i32,
3919        image_id: &str,
3920        instance_type: &str,
3921        owner_id: &str,
3922    ) -> Vec<String> {
3923        let count = count.max(1) as usize;
3924        let now = chrono::Utc::now()
3925            .format("%Y-%m-%dT%H:%M:%S.000Z")
3926            .to_string();
3927        let mut request_ids = Vec::new();
3928        for _ in 0..count {
3929            let spot_id = self.next_spot_id();
3930            let instance_id = self.next_instance_id();
3931            let instance = Instance {
3932                instance_id: instance_id.clone(),
3933                image_id: image_id.to_string(),
3934                instance_type: instance_type.to_string(),
3935                state: InstanceState {
3936                    code: 16,
3937                    name: "running".to_string(),
3938                },
3939                private_ip_address: Some(format!(
3940                    "10.0.{}.{}",
3941                    (self.counters.instance >> 8) & 0xff,
3942                    self.counters.instance & 0xff
3943                )),
3944                public_ip_address: None,
3945                subnet_id: None,
3946                vpc_id: None,
3947                key_name: None,
3948                security_groups: Vec::new(),
3949                launch_time: now.clone(),
3950                tags: HashMap::new(),
3951                iam_instance_profile_arn: None,
3952                monitoring_state: "disabled".to_string(),
3953                placement_az: "us-east-1a".to_string(),
3954                placement_group_name: None,
3955                placement_tenancy: None,
3956                placement_host_id: None,
3957                placement_affinity: None,
3958                placement_partition_number: None,
3959                owner_id: owner_id.to_string(),
3960                classic_link_vpc: None,
3961                private_dns_hostname_type: None,
3962                enable_resource_name_dns_a_record: None,
3963                enable_resource_name_dns_aaaa_record: None,
3964                credit_specification: None,
3965                cpu_options: None,
3966                maintenance_options: None,
3967                network_bandwidth_weighting: None,
3968                lifecycle: Some("spot".to_string()),
3969                product_codes: Vec::new(),
3970                capacity_reservation_specification: None,
3971            };
3972            self.instances.insert(instance_id.clone(), instance);
3973            let req = SpotInstanceRequest {
3974                spot_instance_request_id: spot_id.clone(),
3975                spot_price: spot_price.to_string(),
3976                instance_type: instance_type.to_string(),
3977                image_id: image_id.to_string(),
3978                state: "active".to_string(),
3979                status_code: "fulfilled".to_string(),
3980                instance_id: Some(instance_id),
3981                tags: HashMap::new(),
3982            };
3983            self.spot_requests.insert(spot_id.clone(), req);
3984            request_ids.push(spot_id);
3985        }
3986        request_ids
3987    }
3988
3989    pub fn cancel_spot_instance_requests(&mut self, ids: &[String]) {
3990        for id in ids {
3991            if let Some(req) = self.spot_requests.get_mut(id) {
3992                req.state = "cancelled".to_string();
3993            }
3994        }
3995    }
3996
3997    /// Create the singleton spot datafeed subscription. Returns
3998    /// `AlreadyExists` if one is already present.
3999    pub fn create_spot_datafeed_subscription(
4000        &mut self,
4001        bucket: String,
4002        prefix: Option<String>,
4003        owner_id: String,
4004    ) -> Result<&SpotDatafeedSubscription, Ec2Error> {
4005        if self.spot_datafeed_subscription.is_some() {
4006            return Err(Ec2Error::SpotDatafeedAlreadyExists);
4007        }
4008        self.spot_datafeed_subscription = Some(SpotDatafeedSubscription {
4009            bucket,
4010            prefix,
4011            owner_id,
4012            state: "Active".to_string(),
4013        });
4014        Ok(self.spot_datafeed_subscription.as_ref().unwrap())
4015    }
4016
4017    /// Delete the singleton spot datafeed subscription. No-op if none
4018    /// exists ( real AWS also returns success in that case ).
4019    pub fn delete_spot_datafeed_subscription(&mut self) {
4020        self.spot_datafeed_subscription = None;
4021    }
4022
4023    /// Describe the singleton spot datafeed subscription. Returns
4024    /// `InvalidSpotDatafeedNotFound` when no subscription is set.
4025    pub fn describe_spot_datafeed_subscription(
4026        &self,
4027    ) -> Result<&SpotDatafeedSubscription, Ec2Error> {
4028        self.spot_datafeed_subscription
4029            .as_ref()
4030            .ok_or(Ec2Error::SpotDatafeedNotFound)
4031    }
4032
4033    // --- IAM Instance Profile Association operations ---
4034
4035    pub fn associate_iam_instance_profile(
4036        &mut self,
4037        instance_id: &str,
4038        arn: &str,
4039        name: &str,
4040    ) -> &IamInstanceProfileAssociation {
4041        let assoc_id = self.next_iam_assoc_id();
4042        let assoc = IamInstanceProfileAssociation {
4043            association_id: assoc_id.clone(),
4044            instance_id: instance_id.to_string(),
4045            iam_instance_profile_arn: arn.to_string(),
4046            iam_instance_profile_name: name.to_string(),
4047            state: "associated".to_string(),
4048        };
4049        // Update the instance's IAM profile
4050        if let Some(inst) = self.instances.get_mut(instance_id) {
4051            inst.iam_instance_profile_arn = Some(arn.to_string());
4052        }
4053        self.iam_instance_profile_associations
4054            .insert(assoc_id.clone(), assoc);
4055        self.iam_instance_profile_associations
4056            .get(&assoc_id)
4057            .unwrap()
4058    }
4059
4060    pub fn disassociate_iam_instance_profile(&mut self, assoc_id: &str) -> Result<(), Ec2Error> {
4061        let assoc = self
4062            .iam_instance_profile_associations
4063            .remove(assoc_id)
4064            .ok_or_else(|| Ec2Error::AssociationNotFound(assoc_id.to_string()))?;
4065        if let Some(inst) = self.instances.get_mut(&assoc.instance_id) {
4066            inst.iam_instance_profile_arn = None;
4067        }
4068        Ok(())
4069    }
4070
4071    pub fn replace_iam_instance_profile_association(
4072        &mut self,
4073        assoc_id: &str,
4074        arn: &str,
4075        name: &str,
4076    ) -> Result<(), Ec2Error> {
4077        let assoc = self
4078            .iam_instance_profile_associations
4079            .get_mut(assoc_id)
4080            .ok_or_else(|| Ec2Error::AssociationNotFound(assoc_id.to_string()))?;
4081        assoc.iam_instance_profile_arn = arn.to_string();
4082        assoc.iam_instance_profile_name = name.to_string();
4083        let instance_id = assoc.instance_id.clone();
4084        if let Some(inst) = self.instances.get_mut(&instance_id) {
4085            inst.iam_instance_profile_arn = Some(arn.to_string());
4086        }
4087        Ok(())
4088    }
4089
4090    // --- Additional AMI operations ---
4091
4092    pub fn register_image(
4093        &mut self,
4094        name: &str,
4095        description: &str,
4096        architecture: &str,
4097        virtualization_type: &str,
4098        root_device_name: &str,
4099        tags: Tags,
4100        owner_id: &str,
4101    ) -> String {
4102        let image_id = self.next_ami_id();
4103        let img = Image {
4104            image_id: image_id.clone(),
4105            name: name.to_string(),
4106            description: description.to_string(),
4107            state: "available".to_string(),
4108            owner_id: owner_id.to_string(),
4109            architecture: architecture.to_string(),
4110            image_type: "machine".to_string(),
4111            platform: None,
4112            virtualization_type: virtualization_type.to_string(),
4113            root_device_type: "ebs".to_string(),
4114            root_device_name: root_device_name.to_string(),
4115            public: false,
4116            tags,
4117            source_instance_id: None,
4118            source_instance_type: String::new(),
4119            launch_permissions: Vec::new(),
4120            recycle_bin_state: None,
4121            deprecation_time: None,
4122            recycle_bin_enter_time: None,
4123            product_codes: Vec::new(),
4124            fast_launch_state: None,
4125            deregistration_protection: None,
4126            kernel_id: None,
4127            ramdisk_id: None,
4128            ena_support: None,
4129            sriov_net_support: None,
4130            tpm_support: None,
4131            boot_mode: None,
4132            imds_support: None,
4133            image_location: None,
4134            source_image_id: None,
4135            source_region: None,
4136        };
4137        self.images.insert(image_id.clone(), img);
4138        image_id
4139    }
4140
4141    pub fn copy_image(
4142        &mut self,
4143        source_image_id: &str,
4144        name: &str,
4145        owner_id: &str,
4146    ) -> Result<String, Ec2Error> {
4147        let mut new_img = self
4148            .images
4149            .get(source_image_id)
4150            .ok_or_else(|| Ec2Error::AmiNotFound(source_image_id.to_string()))?
4151            .clone();
4152        let new_id = self.next_ami_id();
4153        new_img.image_id = new_id.clone();
4154        new_img.name = name.to_string();
4155        new_img.owner_id = owner_id.to_string();
4156        new_img.source_image_id = Some(source_image_id.to_string());
4157        self.images.insert(new_id.clone(), new_img);
4158        Ok(new_id)
4159    }
4160
4161    // --- Additional Snapshot operations ---
4162
4163    pub fn copy_snapshot(
4164        &mut self,
4165        source_snapshot_id: &str,
4166        owner_id: &str,
4167    ) -> Result<String, Ec2Error> {
4168        let mut new_snap = self
4169            .snapshots
4170            .get(source_snapshot_id)
4171            .ok_or_else(|| Ec2Error::SnapshotNotFound(source_snapshot_id.to_string()))?
4172            .clone();
4173        let new_id = self.next_snapshot_id();
4174        let now = chrono::Utc::now()
4175            .format("%Y-%m-%dT%H:%M:%S.000Z")
4176            .to_string();
4177        new_snap.snapshot_id = new_id.clone();
4178        new_snap.owner_id = owner_id.to_string();
4179        new_snap.start_time = now;
4180        self.snapshots.insert(new_id.clone(), new_snap);
4181        Ok(new_id)
4182    }
4183
4184    pub fn create_snapshots_from_instance(
4185        &mut self,
4186        instance_id: &str,
4187        owner_id: &str,
4188    ) -> Result<Vec<String>, Ec2Error> {
4189        if !self.instances.contains_key(instance_id) {
4190            return Err(Ec2Error::InstanceNotFound(instance_id.to_string()));
4191        }
4192        let vol_ids: Vec<String> = self
4193            .volumes
4194            .values()
4195            .filter(|v| v.attachments.iter().any(|a| a.instance_id == instance_id))
4196            .map(|v| v.volume_id.clone())
4197            .collect();
4198        let now = chrono::Utc::now()
4199            .format("%Y-%m-%dT%H:%M:%S.000Z")
4200            .to_string();
4201        let mut snap_ids = Vec::new();
4202        for vol_id in vol_ids {
4203            let (vol_size, encrypted) = {
4204                let v = self.volumes.get(&vol_id).unwrap();
4205                (v.size, v.encrypted)
4206            };
4207            let snap_id = self.next_snapshot_id();
4208            let snap = Snapshot {
4209                snapshot_id: snap_id.clone(),
4210                volume_id: vol_id,
4211                volume_size: vol_size,
4212                state: "completed".to_string(),
4213                description: format!("Created by CreateSnapshots for instance {instance_id}"),
4214                start_time: now.clone(),
4215                progress: "100%".to_string(),
4216                owner_id: owner_id.to_string(),
4217                encrypted,
4218                tags: HashMap::new(),
4219                lock_state: "none".to_string(),
4220                lock_duration: None,
4221                lock_created_on: None,
4222                lock_expires_on: None,
4223                lock_duration_start_time: None,
4224                cool_off_period: None,
4225                cool_off_period_expires_on: None,
4226                storage_tier: "standard".to_string(),
4227                last_tiering_operation_status: None,
4228                fast_snapshot_restore_states: Vec::new(),
4229            };
4230            self.snapshots.insert(snap_id.clone(), snap);
4231            snap_ids.push(snap_id);
4232        }
4233        Ok(snap_ids)
4234    }
4235
4236    // --- VPC endpoint modify ---
4237
4238    pub fn modify_vpc_endpoint(&mut self, endpoint_id: &str) -> Result<(), Ec2Error> {
4239        if !self.vpc_endpoints.contains_key(endpoint_id) {
4240            return Err(Ec2Error::VpcEndpointNotFound(endpoint_id.to_string()));
4241        }
4242        Ok(())
4243    }
4244
4245    // --- Dedicated Host operations ---
4246
4247    pub fn allocate_hosts(
4248        &mut self,
4249        availability_zone: &str,
4250        instance_type: Option<String>,
4251        quantity: usize,
4252        auto_placement: &str,
4253        host_recovery: &str,
4254        tags: Tags,
4255    ) -> Vec<String> {
4256        let now = chrono::Utc::now()
4257            .format("%Y-%m-%dT%H:%M:%S.000Z")
4258            .to_string();
4259        let _ = now;
4260        let mut ids = Vec::new();
4261        for _ in 0..quantity {
4262            let host_id = self.next_host_id();
4263            let host = DedicatedHost {
4264                host_id: host_id.clone(),
4265                availability_zone: availability_zone.to_string(),
4266                instance_type: instance_type.clone(),
4267                auto_placement: auto_placement.to_string(),
4268                host_recovery: host_recovery.to_string(),
4269                state: "available".to_string(),
4270                tags: tags.clone(),
4271            };
4272            self.dedicated_hosts.insert(host_id.clone(), host);
4273            ids.push(host_id);
4274        }
4275        ids
4276    }
4277
4278    pub fn release_hosts(&mut self, host_ids: &[String]) -> Vec<String> {
4279        let mut successful = Vec::new();
4280        for id in host_ids {
4281            if let Some(h) = self.dedicated_hosts.get_mut(id) {
4282                h.state = "released".to_string();
4283                successful.push(id.clone());
4284            }
4285        }
4286        successful
4287    }
4288
4289    pub fn modify_hosts(&mut self, host_ids: &[String]) -> Vec<String> {
4290        let mut successful = Vec::new();
4291        for id in host_ids {
4292            if self.dedicated_hosts.contains_key(id) {
4293                successful.push(id.clone());
4294            }
4295        }
4296        successful
4297    }
4298
4299    // --- EC2 Fleet operations ---
4300
4301    pub fn create_fleet(&mut self, fleet_type: &str, tags: Tags) -> String {
4302        let fleet_id = self.next_fleet_id();
4303        let now = chrono::Utc::now()
4304            .format("%Y-%m-%dT%H:%M:%S.000Z")
4305            .to_string();
4306        let fleet = Ec2Fleet {
4307            fleet_id: fleet_id.clone(),
4308            state: "active".to_string(),
4309            fleet_type: fleet_type.to_string(),
4310            create_time: now,
4311            tags,
4312            total_target_capacity: None,
4313            on_demand_target_capacity: None,
4314            spot_target_capacity: None,
4315            context: None,
4316        };
4317        self.ec2_fleets.insert(fleet_id.clone(), fleet);
4318        fleet_id
4319    }
4320
4321    pub fn delete_fleets(&mut self, fleet_ids: &[String]) -> Vec<String> {
4322        let mut successful = Vec::new();
4323        for id in fleet_ids {
4324            if let Some(f) = self.ec2_fleets.get_mut(id) {
4325                f.state = "deleted".to_string();
4326                successful.push(id.clone());
4327            }
4328        }
4329        successful
4330    }
4331
4332    // --- VPC Endpoint Service Configuration operations ---
4333
4334    pub fn create_vpc_endpoint_service_configuration(
4335        &mut self,
4336        acceptance_required: bool,
4337        network_load_balancer_arns: Vec<String>,
4338        gateway_load_balancer_arns: Vec<String>,
4339        tags: Tags,
4340    ) -> &VpcEndpointServiceConfiguration {
4341        let svc_id = self.next_vpce_svc_id();
4342        let svc_name = format!("com.amazonaws.vpce.us-east-1.{svc_id}");
4343        let svc_type = if !gateway_load_balancer_arns.is_empty() {
4344            "GatewayLoadBalancer"
4345        } else {
4346            "Interface"
4347        };
4348        let config = VpcEndpointServiceConfiguration {
4349            service_id: svc_id.clone(),
4350            service_name: svc_name,
4351            service_type: svc_type.to_string(),
4352            acceptance_required,
4353            state: "available".to_string(),
4354            network_load_balancer_arns,
4355            gateway_load_balancer_arns,
4356            allowed_principals: Vec::new(),
4357            tags,
4358            payer_responsibility: Some("ServiceOwner".to_string()),
4359            private_dns_state: None,
4360        };
4361        self.vpc_endpoint_service_configs
4362            .insert(svc_id.clone(), config);
4363        self.vpc_endpoint_service_configs.get(&svc_id).unwrap()
4364    }
4365
4366    /// Seed a pending endpoint connection -- used by tests, by the
4367    /// `CreateVpcEndpoint` handler when the endpoint targets a service that
4368    /// requires acceptance, and by Terraform converters.
4369    pub fn upsert_vpc_endpoint_connection(
4370        &mut self,
4371        service_id: &str,
4372        endpoint_id: &str,
4373        owner: &str,
4374        state: &str,
4375    ) {
4376        self.vpc_endpoint_connections.insert(
4377            (service_id.to_string(), endpoint_id.to_string()),
4378            VpcEndpointConnection {
4379                service_id: service_id.to_string(),
4380                vpc_endpoint_id: endpoint_id.to_string(),
4381                vpc_endpoint_owner: owner.to_string(),
4382                vpc_endpoint_state: state.to_string(),
4383                creation_timestamp: chrono::Utc::now()
4384                    .format("%Y-%m-%dT%H:%M:%S.000Z")
4385                    .to_string(),
4386            },
4387        );
4388    }
4389
4390    pub fn accept_vpc_endpoint_connections(
4391        &mut self,
4392        service_id: &str,
4393        endpoint_ids: &[String],
4394    ) -> Result<Vec<String>, Ec2Error> {
4395        if !self.vpc_endpoint_service_configs.contains_key(service_id) {
4396            return Err(Ec2Error::VpcEndpointServiceNotFound(service_id.to_string()));
4397        }
4398        let mut accepted = Vec::new();
4399        for ep in endpoint_ids {
4400            let key = (service_id.to_string(), ep.clone());
4401            if let Some(conn) = self.vpc_endpoint_connections.get_mut(&key) {
4402                if conn.vpc_endpoint_state == "pendingAcceptance" {
4403                    conn.vpc_endpoint_state = "available".to_string();
4404                    accepted.push(ep.clone());
4405                }
4406            }
4407        }
4408        Ok(accepted)
4409    }
4410
4411    pub fn reject_vpc_endpoint_connections(
4412        &mut self,
4413        service_id: &str,
4414        endpoint_ids: &[String],
4415    ) -> Result<Vec<String>, Ec2Error> {
4416        if !self.vpc_endpoint_service_configs.contains_key(service_id) {
4417            return Err(Ec2Error::VpcEndpointServiceNotFound(service_id.to_string()));
4418        }
4419        let mut rejected = Vec::new();
4420        for ep in endpoint_ids {
4421            let key = (service_id.to_string(), ep.clone());
4422            if let Some(conn) = self.vpc_endpoint_connections.get_mut(&key) {
4423                if conn.vpc_endpoint_state == "pendingAcceptance" {
4424                    conn.vpc_endpoint_state = "rejected".to_string();
4425                    rejected.push(ep.clone());
4426                }
4427            }
4428        }
4429        Ok(rejected)
4430    }
4431
4432    pub fn modify_vpc_endpoint_service_payer_responsibility(
4433        &mut self,
4434        service_id: &str,
4435        payer_responsibility: &str,
4436    ) -> Result<(), Ec2Error> {
4437        let cfg = self
4438            .vpc_endpoint_service_configs
4439            .get_mut(service_id)
4440            .ok_or_else(|| Ec2Error::VpcEndpointServiceNotFound(service_id.to_string()))?;
4441        cfg.payer_responsibility = Some(payer_responsibility.to_string());
4442        Ok(())
4443    }
4444
4445    pub fn start_vpc_endpoint_service_private_dns_verification(
4446        &mut self,
4447        service_id: &str,
4448    ) -> Result<(), Ec2Error> {
4449        let cfg = self
4450            .vpc_endpoint_service_configs
4451            .get_mut(service_id)
4452            .ok_or_else(|| Ec2Error::VpcEndpointServiceNotFound(service_id.to_string()))?;
4453        // Mock: AWS asynchronously transitions verifying -> verified.
4454        // We collapse to "verified" immediately so callers can observe the
4455        // terminal state via DescribeVpcEndpointServiceConfigurations.
4456        cfg.private_dns_state = Some("verified".to_string());
4457        Ok(())
4458    }
4459
4460    // --- VPC Endpoint Connection Notification operations ---
4461
4462    fn next_vpc_endpoint_connection_notification_id(&mut self) -> String {
4463        self.counters.vpc_endpoint_connection_notification += 1;
4464        format!(
4465            "vpce-notif-{:08x}",
4466            self.counters.vpc_endpoint_connection_notification
4467        )
4468    }
4469
4470    pub fn create_vpc_endpoint_connection_notification(
4471        &mut self,
4472        sns_topic_arn: &str,
4473        connection_events: Vec<String>,
4474        service_id: Option<String>,
4475        vpc_endpoint_id: Option<String>,
4476    ) -> &VpcEndpointConnectionNotification {
4477        let id = self.next_vpc_endpoint_connection_notification_id();
4478        let notification_type = if vpc_endpoint_id.is_some() {
4479            "Endpoint".to_string()
4480        } else {
4481            "Topic".to_string()
4482        };
4483        let notification = VpcEndpointConnectionNotification {
4484            connection_notification_id: id.clone(),
4485            connection_notification_arn: sns_topic_arn.to_string(),
4486            connection_events,
4487            connection_notification_state: "Enabled".to_string(),
4488            connection_notification_type: notification_type,
4489            service_id,
4490            vpc_endpoint_id,
4491        };
4492        self.vpc_endpoint_connection_notifications
4493            .insert(id.clone(), notification);
4494        self.vpc_endpoint_connection_notifications.get(&id).unwrap()
4495    }
4496
4497    pub fn delete_vpc_endpoint_connection_notifications(&mut self, ids: &[String]) -> Vec<String> {
4498        let mut not_found = Vec::new();
4499        for id in ids {
4500            if self
4501                .vpc_endpoint_connection_notifications
4502                .remove(id)
4503                .is_none()
4504            {
4505                not_found.push(id.clone());
4506            }
4507        }
4508        not_found
4509    }
4510
4511    pub fn modify_vpc_endpoint_connection_notification(
4512        &mut self,
4513        id: &str,
4514        connection_notification_arn: Option<String>,
4515        connection_events: Option<Vec<String>>,
4516    ) -> Result<(), Ec2Error> {
4517        let n = self
4518            .vpc_endpoint_connection_notifications
4519            .get_mut(id)
4520            .ok_or_else(|| {
4521                Ec2Error::InvalidVpcEndpointConnectionNotificationNotFound(id.to_string())
4522            })?;
4523        if let Some(arn) = connection_notification_arn {
4524            n.connection_notification_arn = arn;
4525        }
4526        if let Some(evs) = connection_events {
4527            n.connection_events = evs;
4528        }
4529        Ok(())
4530    }
4531
4532    // --- VPC Block Public Access operations ---
4533
4534    fn next_vpc_block_public_access_exclusion_id(&mut self) -> String {
4535        self.counters.vpc_block_public_access_exclusion += 1;
4536        format!(
4537            "vpcbpa-exclusion-{:08x}",
4538            self.counters.vpc_block_public_access_exclusion
4539        )
4540    }
4541
4542    pub fn create_vpc_block_public_access_exclusion(
4543        &mut self,
4544        resource_arn: &str,
4545        internet_gateway_exclusion_mode: &str,
4546        tags: Tags,
4547    ) -> &VpcBlockPublicAccessExclusion {
4548        let id = self.next_vpc_block_public_access_exclusion_id();
4549        let now = chrono::Utc::now()
4550            .format("%Y-%m-%dT%H:%M:%S.000Z")
4551            .to_string();
4552        let ex = VpcBlockPublicAccessExclusion {
4553            exclusion_id: id.clone(),
4554            internet_gateway_exclusion_mode: internet_gateway_exclusion_mode.to_string(),
4555            resource_arn: resource_arn.to_string(),
4556            state: "create-complete".to_string(),
4557            creation_timestamp: now.clone(),
4558            last_update_timestamp: now,
4559            tags,
4560        };
4561        self.vpc_block_public_access_exclusions
4562            .insert(id.clone(), ex);
4563        self.vpc_block_public_access_exclusions.get(&id).unwrap()
4564    }
4565
4566    pub fn delete_vpc_block_public_access_exclusion(
4567        &mut self,
4568        id: &str,
4569    ) -> Result<VpcBlockPublicAccessExclusion, Ec2Error> {
4570        let mut ex = self
4571            .vpc_block_public_access_exclusions
4572            .remove(id)
4573            .ok_or_else(|| {
4574                Ec2Error::InvalidVpcBlockPublicAccessExclusionNotFound(id.to_string())
4575            })?;
4576        ex.state = "delete-complete".to_string();
4577        ex.last_update_timestamp = chrono::Utc::now()
4578            .format("%Y-%m-%dT%H:%M:%S.000Z")
4579            .to_string();
4580        Ok(ex)
4581    }
4582
4583    pub fn modify_vpc_block_public_access_exclusion(
4584        &mut self,
4585        id: &str,
4586        new_mode: &str,
4587    ) -> Result<&VpcBlockPublicAccessExclusion, Ec2Error> {
4588        let ex = self
4589            .vpc_block_public_access_exclusions
4590            .get_mut(id)
4591            .ok_or_else(|| {
4592                Ec2Error::InvalidVpcBlockPublicAccessExclusionNotFound(id.to_string())
4593            })?;
4594        ex.internet_gateway_exclusion_mode = new_mode.to_string();
4595        ex.state = "update-complete".to_string();
4596        ex.last_update_timestamp = chrono::Utc::now()
4597            .format("%Y-%m-%dT%H:%M:%S.000Z")
4598            .to_string();
4599        Ok(self.vpc_block_public_access_exclusions.get(id).unwrap())
4600    }
4601
4602    pub fn modify_vpc_block_public_access_options(
4603        &mut self,
4604        account_id: &str,
4605        region: &str,
4606        block_mode: &str,
4607    ) -> &VpcBlockPublicAccessOptions {
4608        let now = chrono::Utc::now()
4609            .format("%Y-%m-%dT%H:%M:%S.000Z")
4610            .to_string();
4611        self.vpc_block_public_access_options = Some(VpcBlockPublicAccessOptions {
4612            aws_account_id: account_id.to_string(),
4613            aws_region: region.to_string(),
4614            internet_gateway_block_mode: block_mode.to_string(),
4615            state: "update-complete".to_string(),
4616            last_update_timestamp: now,
4617        });
4618        self.vpc_block_public_access_options.as_ref().unwrap()
4619    }
4620
4621    // --- VPC Encryption Control operations ---
4622
4623    fn next_vpc_encryption_control_id(&mut self) -> String {
4624        self.counters.vpc_encryption_control += 1;
4625        format!(
4626            "vpc-encryption-control-{:08x}",
4627            self.counters.vpc_encryption_control
4628        )
4629    }
4630
4631    pub fn create_vpc_encryption_control(
4632        &mut self,
4633        vpc_id: &str,
4634        tags: Tags,
4635    ) -> Result<&VpcEncryptionControl, Ec2Error> {
4636        if !self.vpcs.contains_key(vpc_id) {
4637            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
4638        }
4639        let id = self.next_vpc_encryption_control_id();
4640        let now = chrono::Utc::now()
4641            .format("%Y-%m-%dT%H:%M:%S.000Z")
4642            .to_string();
4643        let mode = "monitor".to_string();
4644        let vec = VpcEncryptionControl {
4645            vpc_encryption_control_id: id.clone(),
4646            vpc_id: vpc_id.to_string(),
4647            mode: mode.clone(),
4648            state: "monitor-complete".to_string(),
4649            mode_history: vec![(mode, now)],
4650            tags,
4651        };
4652        self.vpc_encryption_controls.insert(id.clone(), vec);
4653        Ok(self.vpc_encryption_controls.get(&id).unwrap())
4654    }
4655
4656    pub fn delete_vpc_encryption_control(
4657        &mut self,
4658        id: &str,
4659    ) -> Result<VpcEncryptionControl, Ec2Error> {
4660        let mut vec = self
4661            .vpc_encryption_controls
4662            .remove(id)
4663            .ok_or_else(|| Ec2Error::InvalidVpcEncryptionControlNotFound(id.to_string()))?;
4664        vec.state = "delete-complete".to_string();
4665        Ok(vec)
4666    }
4667
4668    pub fn modify_vpc_encryption_control(
4669        &mut self,
4670        id: &str,
4671        new_mode: Option<&str>,
4672    ) -> Result<&VpcEncryptionControl, Ec2Error> {
4673        let vec = self
4674            .vpc_encryption_controls
4675            .get_mut(id)
4676            .ok_or_else(|| Ec2Error::InvalidVpcEncryptionControlNotFound(id.to_string()))?;
4677        if let Some(m) = new_mode {
4678            let now = chrono::Utc::now()
4679                .format("%Y-%m-%dT%H:%M:%S.000Z")
4680                .to_string();
4681            vec.mode = m.to_string();
4682            vec.state = match m {
4683                "enforce" => "enforce-complete".to_string(),
4684                _ => "monitor-complete".to_string(),
4685            };
4686            vec.mode_history.push((m.to_string(), now));
4687        }
4688        Ok(self.vpc_encryption_controls.get(id).unwrap())
4689    }
4690
4691    pub fn delete_vpc_endpoint_service_configurations(
4692        &mut self,
4693        svc_ids: &[String],
4694    ) -> Vec<String> {
4695        let mut removed = Vec::new();
4696        for id in svc_ids {
4697            if self.vpc_endpoint_service_configs.remove(id).is_some() {
4698                removed.push(id.clone());
4699            }
4700        }
4701        removed
4702    }
4703
4704    pub fn modify_vpc_endpoint_service_permissions(
4705        &mut self,
4706        svc_id: &str,
4707        add_arns: Vec<String>,
4708        remove_arns: Vec<String>,
4709    ) -> Result<(), Ec2Error> {
4710        let config = self
4711            .vpc_endpoint_service_configs
4712            .get_mut(svc_id)
4713            .ok_or_else(|| Ec2Error::VpcEndpointServiceNotFound(svc_id.to_string()))?;
4714        for arn in add_arns {
4715            if !config.allowed_principals.contains(&arn) {
4716                config.allowed_principals.push(arn);
4717            }
4718        }
4719        config
4720            .allowed_principals
4721            .retain(|p| !remove_arns.contains(p));
4722        Ok(())
4723    }
4724
4725    // --- Spot Fleet operations ---
4726
4727    pub fn request_spot_fleet(
4728        &mut self,
4729        target_capacity: i32,
4730        iam_fleet_role: &str,
4731        tags: Tags,
4732    ) -> String {
4733        let id = self.next_spot_fleet_id();
4734        let now = chrono::Utc::now()
4735            .format("%Y-%m-%dT%H:%M:%S.000Z")
4736            .to_string();
4737        let req = SpotFleetRequest {
4738            spot_fleet_request_id: id.clone(),
4739            spot_fleet_request_state: "active".to_string(),
4740            target_capacity,
4741            iam_fleet_role: iam_fleet_role.to_string(),
4742            create_time: now,
4743            tags,
4744        };
4745        self.spot_fleet_requests.insert(id.clone(), req);
4746        id
4747    }
4748
4749    pub fn cancel_spot_fleet_requests(&mut self, ids: &[String]) -> Vec<String> {
4750        let mut successful = Vec::new();
4751        for id in ids {
4752            if let Some(r) = self.spot_fleet_requests.get_mut(id) {
4753                r.spot_fleet_request_state = "cancelled".to_string();
4754                successful.push(id.clone());
4755            }
4756        }
4757        successful
4758    }
4759
4760    // --- Subnet CIDR Reservation operations ---
4761
4762    pub fn create_subnet_cidr_reservation(
4763        &mut self,
4764        subnet_id: &str,
4765        cidr: &str,
4766        reservation_type: &str,
4767        description: &str,
4768        owner_id: &str,
4769    ) -> Result<SubnetCidrReservationEntry, Ec2Error> {
4770        if !self.subnets.contains_key(subnet_id) {
4771            return Err(Ec2Error::SubnetNotFound(subnet_id.to_string()));
4772        }
4773        let id = self.next_subnet_cidr_res_id();
4774        let entry = SubnetCidrReservationEntry {
4775            reservation_id: id.clone(),
4776            subnet_id: subnet_id.to_string(),
4777            cidr: cidr.to_string(),
4778            reservation_type: reservation_type.to_string(),
4779            description: description.to_string(),
4780            owner_id: owner_id.to_string(),
4781        };
4782        self.subnet_cidr_reservations.insert(id, entry.clone());
4783        Ok(entry)
4784    }
4785
4786    pub fn delete_subnet_cidr_reservation(
4787        &mut self,
4788        reservation_id: &str,
4789    ) -> Result<SubnetCidrReservationEntry, Ec2Error> {
4790        self.subnet_cidr_reservations
4791            .remove(reservation_id)
4792            .ok_or_else(|| Ec2Error::SubnetCidrReservationNotFound(reservation_id.to_string()))
4793    }
4794
4795    // --- Subnet IPv6 CIDR operations ---
4796
4797    pub fn associate_subnet_cidr_block(
4798        &mut self,
4799        subnet_id: &str,
4800        ipv6_cidr_block: &str,
4801    ) -> Result<(String, String), Ec2Error> {
4802        let subnet = self
4803            .subnets
4804            .get_mut(subnet_id)
4805            .ok_or_else(|| Ec2Error::SubnetNotFound(subnet_id.to_string()))?;
4806        let assoc_id = {
4807            self.counters.subnet_ipv6_assoc += 1;
4808            format!("subnet-cidr-assoc-{:08x}", self.counters.subnet_ipv6_assoc)
4809        };
4810        subnet.ipv6_cidr_blocks.push(SubnetIpv6CidrAssoc {
4811            association_id: assoc_id.clone(),
4812            ipv6_cidr_block: ipv6_cidr_block.to_string(),
4813            state: "associated".to_string(),
4814        });
4815        Ok((assoc_id, ipv6_cidr_block.to_string()))
4816    }
4817
4818    pub fn disassociate_subnet_cidr_block(
4819        &mut self,
4820        association_id: &str,
4821    ) -> Result<(String, String), Ec2Error> {
4822        for subnet in self.subnets.values_mut() {
4823            if let Some(pos) = subnet
4824                .ipv6_cidr_blocks
4825                .iter()
4826                .position(|a| a.association_id == association_id)
4827            {
4828                let cidr = subnet.ipv6_cidr_blocks[pos].ipv6_cidr_block.clone();
4829                let subnet_id = subnet.subnet_id.clone();
4830                subnet.ipv6_cidr_blocks.remove(pos);
4831                return Ok((subnet_id, cidr));
4832            }
4833        }
4834        Err(Ec2Error::AssociationNotFound(association_id.to_string()))
4835    }
4836
4837    // --- Default VPC / Subnet operations ---
4838
4839    pub fn create_default_vpc(&mut self) -> Result<&Vpc, Ec2Error> {
4840        // Check if a default VPC already exists
4841        if self.vpcs.values().any(|v| v.is_default) {
4842            return Err(Ec2Error::DefaultVpcAlreadyExists);
4843        }
4844        let vpc_id = self.next_vpc_id();
4845        let dhcp_options_id = format!("dopt-{:08x}", self.counters.vpc);
4846        let vpc = Vpc {
4847            vpc_id: vpc_id.clone(),
4848            cidr_block: "172.31.0.0/16".to_string(),
4849            state: "available".to_string(),
4850            dhcp_options_id,
4851            instance_tenancy: "default".to_string(),
4852            is_default: true,
4853            enable_dns_hostnames: true,
4854            enable_dns_support: true,
4855            secondary_cidr_blocks: Vec::new(),
4856            tags: HashMap::new(),
4857            classic_link_enabled: false,
4858        };
4859        self.vpcs.insert(vpc_id.clone(), vpc);
4860        Ok(self.vpcs.get(&vpc_id).unwrap())
4861    }
4862
4863    pub fn create_default_subnet(&mut self, availability_zone: &str) -> Result<&Subnet, Ec2Error> {
4864        let default_vpc_id = self
4865            .vpcs
4866            .values()
4867            .find(|v| v.is_default)
4868            .map(|v| v.vpc_id.clone())
4869            .ok_or(Ec2Error::DefaultVpcNotFound)?;
4870        let subnet_id = self.next_subnet_id();
4871        let subnet = Subnet {
4872            subnet_id: subnet_id.clone(),
4873            vpc_id: default_vpc_id,
4874            cidr_block: "172.31.0.0/20".to_string(),
4875            availability_zone: availability_zone.to_string(),
4876            state: "available".to_string(),
4877            available_ip_address_count: 4091,
4878            map_public_ip_on_launch: true,
4879            ipv6_cidr_blocks: Vec::new(),
4880            tags: HashMap::new(),
4881        };
4882        self.subnets.insert(subnet_id.clone(), subnet);
4883        Ok(self.subnets.get(&subnet_id).unwrap())
4884    }
4885
4886    fn next_pg_id(&mut self) -> String {
4887        self.counters.placement_group += 1;
4888        format!("pg-{:08x}", self.counters.placement_group)
4889    }
4890
4891    pub fn create_placement_group(
4892        &mut self,
4893        group_name: &str,
4894        strategy: &str,
4895        partition_count: Option<i32>,
4896        spread_level: Option<String>,
4897        account_id: &str,
4898        region: &str,
4899        tags: Tags,
4900    ) -> Result<&PlacementGroup, Ec2Error> {
4901        if self
4902            .placement_groups
4903            .values()
4904            .any(|g| g.group_name == group_name)
4905        {
4906            return Err(Ec2Error::PlacementGroupAlreadyExists(
4907                group_name.to_string(),
4908            ));
4909        }
4910        match strategy {
4911            "cluster" | "spread" | "partition" => {}
4912            other => {
4913                return Err(Ec2Error::InvalidParameterValue(format!(
4914                    "Invalid placement strategy: {other}"
4915                )));
4916            }
4917        }
4918        if strategy == "partition" {
4919            let pc = partition_count.unwrap_or(2);
4920            if !(2..=7).contains(&pc) {
4921                return Err(Ec2Error::InvalidParameterValue(
4922                    "Partition count must be between 2 and 7".to_string(),
4923                ));
4924            }
4925        }
4926        let resolved_partition_count = if strategy == "partition" {
4927            Some(partition_count.unwrap_or(2))
4928        } else {
4929            None
4930        };
4931        let group_id = self.next_pg_id();
4932        let group_arn = format!("arn:aws:ec2:{region}:{account_id}:placement-group/{group_name}");
4933        let pg = PlacementGroup {
4934            group_id: group_id.clone(),
4935            group_name: group_name.to_string(),
4936            group_arn,
4937            strategy: strategy.to_string(),
4938            state: "available".to_string(),
4939            partition_count: resolved_partition_count,
4940            spread_level,
4941            tags,
4942        };
4943        self.placement_groups.insert(group_id.clone(), pg);
4944        Ok(self.placement_groups.get(&group_id).unwrap())
4945    }
4946
4947    pub fn delete_placement_group(&mut self, group_name: &str) -> Result<(), Ec2Error> {
4948        let id = self
4949            .placement_groups
4950            .values()
4951            .find(|g| g.group_name == group_name)
4952            .map(|g| g.group_id.clone())
4953            .ok_or_else(|| Ec2Error::PlacementGroupNotFound(group_name.to_string()))?;
4954        self.placement_groups.remove(&id);
4955        Ok(())
4956    }
4957
4958    fn next_eni_permission_id(&mut self) -> String {
4959        self.counters.eni_permission += 1;
4960        format!("eni-perm-{:08x}", self.counters.eni_permission)
4961    }
4962
4963    pub fn create_network_interface_permission(
4964        &mut self,
4965        network_interface_id: &str,
4966        aws_account_id: Option<String>,
4967        aws_service: Option<String>,
4968        permission: &str,
4969    ) -> Result<&NetworkInterfacePermission, Ec2Error> {
4970        if !self.network_interfaces.contains_key(network_interface_id) {
4971            return Err(Ec2Error::NetworkInterfaceNotFound(
4972                network_interface_id.to_string(),
4973            ));
4974        }
4975        match permission {
4976            "INSTANCE-ATTACH" | "EIP-ASSOCIATE" => {}
4977            other => {
4978                return Err(Ec2Error::InvalidParameterValue(format!(
4979                    "Invalid permission: {other}"
4980                )));
4981            }
4982        }
4983        let id = self.next_eni_permission_id();
4984        let perm = NetworkInterfacePermission {
4985            network_interface_permission_id: id.clone(),
4986            network_interface_id: network_interface_id.to_string(),
4987            aws_account_id,
4988            aws_service,
4989            permission: permission.to_string(),
4990            permission_state: "GRANTED".to_string(),
4991        };
4992        self.network_interface_permissions.insert(id.clone(), perm);
4993        Ok(self.network_interface_permissions.get(&id).unwrap())
4994    }
4995
4996    pub fn delete_network_interface_permission(
4997        &mut self,
4998        network_interface_permission_id: &str,
4999    ) -> Result<(), Ec2Error> {
5000        if self
5001            .network_interface_permissions
5002            .remove(network_interface_permission_id)
5003            .is_none()
5004        {
5005            return Err(Ec2Error::NetworkInterfacePermissionNotFound(
5006                network_interface_permission_id.to_string(),
5007            ));
5008        }
5009        Ok(())
5010    }
5011
5012    fn next_instance_connect_endpoint_id(&mut self) -> String {
5013        self.counters.instance_connect_endpoint += 1;
5014        format!("eice-{:08x}", self.counters.instance_connect_endpoint)
5015    }
5016
5017    #[allow(clippy::too_many_arguments)]
5018    pub fn create_instance_connect_endpoint(
5019        &mut self,
5020        subnet_id: &str,
5021        security_group_ids: Vec<String>,
5022        preserve_client_ip: bool,
5023        ip_address_type: Option<String>,
5024        account_id: &str,
5025        region: &str,
5026        tags: Tags,
5027    ) -> Result<&InstanceConnectEndpoint, Ec2Error> {
5028        let subnet = self
5029            .subnets
5030            .get(subnet_id)
5031            .ok_or_else(|| Ec2Error::SubnetNotFound(subnet_id.to_string()))?;
5032        let vpc_id = subnet.vpc_id.clone();
5033        let availability_zone = subnet.availability_zone.clone();
5034        let id = self.next_instance_connect_endpoint_id();
5035        let arn = format!("arn:aws:ec2:{region}:{account_id}:instance-connect-endpoint/{id}");
5036        let dns_name = format!("{id}.{region}.ec2-instance-connect-endpoint.amazonaws.com");
5037        let fips_dns_name =
5038            format!("{id}.{region}.fips.ec2-instance-connect-endpoint.amazonaws.com");
5039        let ice = InstanceConnectEndpoint {
5040            instance_connect_endpoint_id: id.clone(),
5041            instance_connect_endpoint_arn: arn,
5042            subnet_id: subnet_id.to_string(),
5043            vpc_id,
5044            availability_zone,
5045            state: "create-complete".to_string(),
5046            created_at: "1970-01-01T00:00:00Z".to_string(),
5047            preserve_client_ip,
5048            security_group_ids,
5049            network_interface_ids: vec![],
5050            dns_name,
5051            fips_dns_name,
5052            ip_address_type: ip_address_type.unwrap_or_else(|| "ipv4".to_string()),
5053            owner_id: account_id.to_string(),
5054            tags,
5055        };
5056        self.instance_connect_endpoints.insert(id.clone(), ice);
5057        Ok(self.instance_connect_endpoints.get(&id).unwrap())
5058    }
5059
5060    pub fn delete_instance_connect_endpoint(&mut self, endpoint_id: &str) -> Result<(), Ec2Error> {
5061        let ice = self
5062            .instance_connect_endpoints
5063            .get_mut(endpoint_id)
5064            .ok_or_else(|| Ec2Error::InstanceConnectEndpointNotFound(endpoint_id.to_string()))?;
5065        ice.state = "delete-complete".to_string();
5066        // Keep entry briefly so describe can find it; in real AWS they linger.
5067        // For simplicity, remove from map.
5068        self.instance_connect_endpoints.remove(endpoint_id);
5069        Ok(())
5070    }
5071
5072    fn next_capacity_reservation_id(&mut self) -> String {
5073        self.counters.capacity_reservation += 1;
5074        format!("cr-{:08x}", self.counters.capacity_reservation)
5075    }
5076
5077    #[allow(clippy::too_many_arguments)]
5078    pub fn create_capacity_reservation(
5079        &mut self,
5080        instance_type: &str,
5081        instance_platform: &str,
5082        availability_zone: String,
5083        instance_count: i32,
5084        ebs_optimized: bool,
5085        ephemeral_storage: bool,
5086        tenancy: String,
5087        instance_match_criteria: String,
5088        end_date: Option<String>,
5089        end_date_type: String,
5090        outpost_arn: Option<String>,
5091        placement_group_arn: Option<String>,
5092        account_id: &str,
5093        region: &str,
5094        tags: Tags,
5095    ) -> Result<&CapacityReservation, Ec2Error> {
5096        if instance_count <= 0 {
5097            return Err(Ec2Error::InvalidParameterValue(
5098                "InstanceCount must be > 0".to_string(),
5099            ));
5100        }
5101        match end_date_type.as_str() {
5102            "unlimited" | "limited" => {}
5103            _ => {
5104                return Err(Ec2Error::InvalidParameterValue(format!(
5105                    "Invalid EndDateType: {end_date_type}"
5106                )));
5107            }
5108        }
5109        let id = self.next_capacity_reservation_id();
5110        let arn = format!("arn:aws:ec2:{region}:{account_id}:capacity-reservation/{id}");
5111        let cr = CapacityReservation {
5112            capacity_reservation_id: id.clone(),
5113            capacity_reservation_arn: arn,
5114            owner_id: account_id.to_string(),
5115            instance_type: instance_type.to_string(),
5116            instance_platform: instance_platform.to_string(),
5117            availability_zone,
5118            tenancy,
5119            total_instance_count: instance_count,
5120            available_instance_count: instance_count,
5121            ebs_optimized,
5122            ephemeral_storage,
5123            state: "active".to_string(),
5124            start_date: "1970-01-01T00:00:00Z".to_string(),
5125            end_date,
5126            end_date_type,
5127            instance_match_criteria,
5128            create_date: "1970-01-01T00:00:00Z".to_string(),
5129            outpost_arn,
5130            placement_group_arn,
5131            tags,
5132            pending_billing_owner_account_id: None,
5133            billing_owner_account_id: Some(account_id.to_string()),
5134            target_capacity_reservation_id: None,
5135            reservation_type: Some("default".to_string()),
5136            commitment_info: None,
5137        };
5138        self.capacity_reservations.insert(id.clone(), cr);
5139        Ok(self.capacity_reservations.get(&id).unwrap())
5140    }
5141
5142    pub fn cancel_capacity_reservation(&mut self, id: &str) -> Result<(), Ec2Error> {
5143        let cr = self
5144            .capacity_reservations
5145            .get_mut(id)
5146            .ok_or_else(|| Ec2Error::CapacityReservationNotFound(id.to_string()))?;
5147        cr.state = "cancelled".to_string();
5148        Ok(())
5149    }
5150
5151    pub fn modify_capacity_reservation(
5152        &mut self,
5153        id: &str,
5154        instance_count: Option<i32>,
5155        end_date: Option<String>,
5156        end_date_type: Option<String>,
5157        instance_match_criteria: Option<String>,
5158    ) -> Result<&CapacityReservation, Ec2Error> {
5159        let cr = self
5160            .capacity_reservations
5161            .get_mut(id)
5162            .ok_or_else(|| Ec2Error::CapacityReservationNotFound(id.to_string()))?;
5163        if let Some(c) = instance_count {
5164            if c <= 0 {
5165                return Err(Ec2Error::InvalidParameterValue(
5166                    "InstanceCount must be > 0".to_string(),
5167                ));
5168            }
5169            cr.total_instance_count = c;
5170            cr.available_instance_count = c;
5171        }
5172        if let Some(d) = end_date {
5173            cr.end_date = Some(d);
5174        }
5175        if let Some(t) = end_date_type {
5176            cr.end_date_type = t;
5177        }
5178        if let Some(m) = instance_match_criteria {
5179            cr.instance_match_criteria = m;
5180        }
5181        Ok(self.capacity_reservations.get(id).unwrap())
5182    }
5183
5184    // --- Instance placement modification ---
5185
5186    #[allow(clippy::too_many_arguments)]
5187    pub fn modify_instance_placement(
5188        &mut self,
5189        instance_id: &str,
5190        group_name: Option<String>,
5191        partition_number: Option<i32>,
5192        host_id: Option<String>,
5193        affinity: Option<String>,
5194        tenancy: Option<String>,
5195    ) -> Result<(), Ec2Error> {
5196        let inst = self
5197            .instances
5198            .get_mut(instance_id)
5199            .ok_or_else(|| Ec2Error::InstanceNotFound(instance_id.to_string()))?;
5200        if let Some(g) = group_name {
5201            // An empty string clears the placement group; this matches AWS
5202            // behaviour where ModifyInstancePlacement with GroupName="" removes
5203            // the instance from any placement group.
5204            if g.is_empty() {
5205                inst.placement_group_name = None;
5206            } else {
5207                inst.placement_group_name = Some(g);
5208            }
5209        }
5210        if let Some(p) = partition_number {
5211            inst.placement_partition_number = Some(p);
5212        }
5213        if let Some(h) = host_id {
5214            if h.is_empty() {
5215                inst.placement_host_id = None;
5216            } else {
5217                inst.placement_host_id = Some(h);
5218            }
5219        }
5220        if let Some(a) = affinity {
5221            if a.is_empty() {
5222                inst.placement_affinity = None;
5223            } else {
5224                inst.placement_affinity = Some(a);
5225            }
5226        }
5227        if let Some(t) = tenancy {
5228            if t.is_empty() {
5229                inst.placement_tenancy = None;
5230            } else {
5231                inst.placement_tenancy = Some(t);
5232            }
5233        }
5234        Ok(())
5235    }
5236
5237    // --- Capacity Reservation Fleet operations ---
5238
5239    fn next_capacity_reservation_fleet_id(&mut self) -> String {
5240        self.counters.capacity_reservation_fleet += 1;
5241        format!("crf-{:08x}", self.counters.capacity_reservation_fleet)
5242    }
5243
5244    #[allow(clippy::too_many_arguments)]
5245    pub fn create_capacity_reservation_fleet(
5246        &mut self,
5247        allocation_strategy: Option<String>,
5248        tenancy: Option<String>,
5249        instance_match_criteria: Option<String>,
5250        total_target_capacity: i32,
5251        end_date: Option<String>,
5252        instance_specs: Vec<CapacityReservationFleetInstanceSpec>,
5253        account_id: &str,
5254        region: &str,
5255        tags: Tags,
5256    ) -> Result<&CapacityReservationFleet, Ec2Error> {
5257        if total_target_capacity <= 0 {
5258            return Err(Ec2Error::InvalidParameterValue(
5259                "TotalTargetCapacity must be > 0".to_string(),
5260            ));
5261        }
5262        if instance_specs.is_empty() {
5263            return Err(Ec2Error::InvalidParameterValue(
5264                "At least one InstanceTypeSpecification is required".to_string(),
5265            ));
5266        }
5267        let id = self.next_capacity_reservation_fleet_id();
5268        let arn = format!("arn:aws:ec2:{region}:{account_id}:capacity-reservation-fleet/{id}");
5269        // Sum of specification weights serves as a proxy for fulfilled capacity.
5270        let fulfilled: f64 = instance_specs.iter().map(|s| s.weight.unwrap_or(1.0)).sum();
5271        let fleet = CapacityReservationFleet {
5272            capacity_reservation_fleet_id: id.clone(),
5273            capacity_reservation_fleet_arn: arn,
5274            state: "active".to_string(),
5275            tenancy: tenancy.unwrap_or_else(|| "default".to_string()),
5276            allocation_strategy: allocation_strategy.unwrap_or_else(|| "prioritized".to_string()),
5277            instance_match_criteria: instance_match_criteria.unwrap_or_else(|| "open".to_string()),
5278            total_target_capacity,
5279            total_fulfilled_capacity: fulfilled,
5280            create_time: "1970-01-01T00:00:00Z".to_string(),
5281            end_date,
5282            instance_type_specifications: instance_specs,
5283            tags,
5284        };
5285        self.capacity_reservation_fleets.insert(id.clone(), fleet);
5286        Ok(self.capacity_reservation_fleets.get(&id).unwrap())
5287    }
5288
5289    pub fn cancel_capacity_reservation_fleet(&mut self, id: &str) -> Result<(), Ec2Error> {
5290        let fleet = self
5291            .capacity_reservation_fleets
5292            .get_mut(id)
5293            .ok_or_else(|| Ec2Error::CapacityReservationFleetNotFound(id.to_string()))?;
5294        fleet.state = "cancelled".to_string();
5295        Ok(())
5296    }
5297
5298    pub fn modify_capacity_reservation_fleet(
5299        &mut self,
5300        id: &str,
5301        total_target_capacity: Option<i32>,
5302        end_date: Option<String>,
5303        remove_end_date: Option<bool>,
5304    ) -> Result<&CapacityReservationFleet, Ec2Error> {
5305        let fleet = self
5306            .capacity_reservation_fleets
5307            .get_mut(id)
5308            .ok_or_else(|| Ec2Error::CapacityReservationFleetNotFound(id.to_string()))?;
5309        if let Some(c) = total_target_capacity {
5310            if c <= 0 {
5311                return Err(Ec2Error::InvalidParameterValue(
5312                    "TotalTargetCapacity must be > 0".to_string(),
5313                ));
5314            }
5315            fleet.total_target_capacity = c;
5316        }
5317        if remove_end_date == Some(true) {
5318            fleet.end_date = None;
5319        } else if let Some(d) = end_date {
5320            fleet.end_date = Some(d);
5321        }
5322        Ok(self.capacity_reservation_fleets.get(id).unwrap())
5323    }
5324
5325    // --- COIP Pool operations ---
5326
5327    fn next_coip_pool_id(&mut self) -> String {
5328        self.counters.coip_pool += 1;
5329        format!("ipv4pool-coip-{:08x}", self.counters.coip_pool)
5330    }
5331
5332    pub fn create_coip_pool(
5333        &mut self,
5334        local_gateway_route_table_id: &str,
5335        pool_cidrs: Vec<String>,
5336        account_id: &str,
5337        region: &str,
5338        tags: Tags,
5339    ) -> &CoipPool {
5340        let pool_id = self.next_coip_pool_id();
5341        let arn = format!("arn:aws:ec2:{region}:{account_id}:coip-pool/{pool_id}");
5342        let pool = CoipPool {
5343            pool_id: pool_id.clone(),
5344            pool_arn: arn,
5345            local_gateway_route_table_id: local_gateway_route_table_id.to_string(),
5346            pool_cidrs,
5347            tags,
5348        };
5349        self.coip_pools.insert(pool_id.clone(), pool);
5350        self.coip_pools.get(&pool_id).unwrap()
5351    }
5352
5353    pub fn delete_coip_pool(&mut self, pool_id: &str) -> Result<CoipPool, Ec2Error> {
5354        self.coip_pools
5355            .remove(pool_id)
5356            .ok_or_else(|| Ec2Error::CoipPoolNotFound(pool_id.to_string()))
5357    }
5358
5359    // --- ClassicLink VPC operations ---
5360
5361    pub fn attach_classic_link_vpc(
5362        &mut self,
5363        instance_id: &str,
5364        vpc_id: &str,
5365        groups: Vec<String>,
5366    ) -> Result<(), Ec2Error> {
5367        if !self.vpcs.contains_key(vpc_id) {
5368            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
5369        }
5370        let inst = self
5371            .instances
5372            .get_mut(instance_id)
5373            .ok_or_else(|| Ec2Error::InstanceNotFound(instance_id.to_string()))?;
5374        inst.classic_link_vpc = Some((vpc_id.to_string(), groups));
5375        Ok(())
5376    }
5377
5378    pub fn detach_classic_link_vpc(
5379        &mut self,
5380        instance_id: &str,
5381        vpc_id: &str,
5382    ) -> Result<(), Ec2Error> {
5383        let inst = self
5384            .instances
5385            .get_mut(instance_id)
5386            .ok_or_else(|| Ec2Error::InstanceNotFound(instance_id.to_string()))?;
5387        match &inst.classic_link_vpc {
5388            Some((linked_vpc, _)) if linked_vpc == vpc_id => {
5389                inst.classic_link_vpc = None;
5390                Ok(())
5391            }
5392            _ => Err(Ec2Error::InvalidParameterValue(format!(
5393                "Instance '{instance_id}' is not linked to VPC '{vpc_id}'"
5394            ))),
5395        }
5396    }
5397
5398    // --- Security Group VPC association operations ---
5399
5400    pub fn associate_security_group_vpc(
5401        &mut self,
5402        group_id: &str,
5403        vpc_id: &str,
5404        vpc_owner_id: &str,
5405    ) -> Result<&SecurityGroupVpcAssociation, Ec2Error> {
5406        if !self.security_groups.contains_key(group_id) {
5407            return Err(Ec2Error::SecurityGroupNotFound(group_id.to_string()));
5408        }
5409        let key = (group_id.to_string(), vpc_id.to_string());
5410        let assoc = SecurityGroupVpcAssociation {
5411            group_id: group_id.to_string(),
5412            vpc_id: vpc_id.to_string(),
5413            vpc_owner_id: vpc_owner_id.to_string(),
5414            state: "associated".to_string(),
5415        };
5416        self.security_group_vpc_associations
5417            .insert(key.clone(), assoc);
5418        Ok(self.security_group_vpc_associations.get(&key).unwrap())
5419    }
5420
5421    pub fn disassociate_security_group_vpc(
5422        &mut self,
5423        group_id: &str,
5424        vpc_id: &str,
5425    ) -> Result<SecurityGroupVpcAssociation, Ec2Error> {
5426        let key = (group_id.to_string(), vpc_id.to_string());
5427        self.security_group_vpc_associations
5428            .remove(&key)
5429            .ok_or_else(|| {
5430                Ec2Error::SecurityGroupVpcAssociationNotFound(
5431                    group_id.to_string(),
5432                    vpc_id.to_string(),
5433                )
5434            })
5435    }
5436
5437    // --- Enclave Certificate IAM Role association operations ---
5438
5439    pub fn associate_enclave_certificate_iam_role(
5440        &mut self,
5441        certificate_arn: &str,
5442        role_arn: &str,
5443    ) -> &EnclaveCertificateIamRoleAssociation {
5444        let key = (certificate_arn.to_string(), role_arn.to_string());
5445        // Generate predictable bucket/key/kms ID values for the mock.
5446        let bucket = format!(
5447            "aws-ec2-enclave-certificate-{:016x}",
5448            self.enclave_certificate_iam_role_associations.len() as u64 + 1
5449        );
5450        let object_key = format!("{certificate_arn}/{role_arn}.pem");
5451        let kms_key_id = format!(
5452            "arn:aws:kms:us-east-1:000000000000:key/{:08x}-mock-mock-mock-{:012x}",
5453            self.enclave_certificate_iam_role_associations.len() as u64 + 1,
5454            self.enclave_certificate_iam_role_associations.len() as u64 + 1
5455        );
5456        let assoc = EnclaveCertificateIamRoleAssociation {
5457            certificate_arn: certificate_arn.to_string(),
5458            role_arn: role_arn.to_string(),
5459            certificate_s3_bucket_name: bucket,
5460            certificate_s3_object_key: object_key,
5461            encryption_kms_key_id: kms_key_id,
5462        };
5463        self.enclave_certificate_iam_role_associations
5464            .insert(key.clone(), assoc);
5465        self.enclave_certificate_iam_role_associations
5466            .get(&key)
5467            .unwrap()
5468    }
5469
5470    pub fn disassociate_enclave_certificate_iam_role(
5471        &mut self,
5472        certificate_arn: &str,
5473        role_arn: &str,
5474    ) -> Result<(), Ec2Error> {
5475        let key = (certificate_arn.to_string(), role_arn.to_string());
5476        if self
5477            .enclave_certificate_iam_role_associations
5478            .remove(&key)
5479            .is_none()
5480        {
5481            return Err(Ec2Error::EnclaveCertificateIamRoleAssociationNotFound(
5482                certificate_arn.to_string(),
5483                role_arn.to_string(),
5484            ));
5485        }
5486        Ok(())
5487    }
5488
5489    // --- DNS name option operations ---
5490
5491    pub fn modify_private_dns_name_options(
5492        &mut self,
5493        instance_id: &str,
5494        private_dns_hostname_type: Option<String>,
5495        enable_resource_name_dns_a_record: Option<bool>,
5496        enable_resource_name_dns_aaaa_record: Option<bool>,
5497    ) -> Result<(), Ec2Error> {
5498        let inst = self
5499            .instances
5500            .get_mut(instance_id)
5501            .ok_or_else(|| Ec2Error::InstanceNotFound(instance_id.to_string()))?;
5502        if let Some(t) = private_dns_hostname_type {
5503            inst.private_dns_hostname_type = Some(t);
5504        }
5505        if let Some(v) = enable_resource_name_dns_a_record {
5506            inst.enable_resource_name_dns_a_record = Some(v);
5507        }
5508        if let Some(v) = enable_resource_name_dns_aaaa_record {
5509            inst.enable_resource_name_dns_aaaa_record = Some(v);
5510        }
5511        Ok(())
5512    }
5513
5514    pub fn modify_public_ip_dns_name_options(
5515        &mut self,
5516        network_interface_id: &str,
5517        hostname_type: &str,
5518    ) -> Result<(), Ec2Error> {
5519        let eni = self
5520            .network_interfaces
5521            .get_mut(network_interface_id)
5522            .ok_or_else(|| Ec2Error::NetworkInterfaceNotFound(network_interface_id.to_string()))?;
5523        eni.public_ip_dns_hostname_type = Some(hostname_type.to_string());
5524        Ok(())
5525    }
5526
5527    // --- Mac SIP modification task operations ---
5528
5529    fn next_mac_sip_task_id(&mut self) -> String {
5530        self.counters.mac_sip_task += 1;
5531        format!("macmodification-{:08x}", self.counters.mac_sip_task)
5532    }
5533
5534    #[allow(clippy::too_many_arguments)]
5535    pub fn create_mac_sip_modification_task(
5536        &mut self,
5537        instance_id: &str,
5538        apple_internal: Option<String>,
5539        base_system: Option<String>,
5540        debugging_restrictions: Option<String>,
5541        dtrace_restrictions: Option<String>,
5542        filesystem_protections: Option<String>,
5543        kext_signing: Option<String>,
5544        nvram_protections: Option<String>,
5545        tags: Tags,
5546    ) -> Result<&MacSipModificationTask, Ec2Error> {
5547        if !self.instances.contains_key(instance_id) {
5548            return Err(Ec2Error::InstanceNotFound(instance_id.to_string()));
5549        }
5550        let task_id = self.next_mac_sip_task_id();
5551        let now = chrono::Utc::now()
5552            .format("%Y-%m-%dT%H:%M:%S.000Z")
5553            .to_string();
5554        let task = MacSipModificationTask {
5555            task_id: task_id.clone(),
5556            instance_id: instance_id.to_string(),
5557            task_type: "sip-modification".to_string(),
5558            task_state: "in-progress".to_string(),
5559            start_time: now,
5560            apple_internal,
5561            base_system,
5562            debugging_restrictions,
5563            dtrace_restrictions,
5564            filesystem_protections,
5565            kext_signing,
5566            nvram_protections,
5567            status: Some("pending".to_string()),
5568            tags,
5569        };
5570        self.mac_sip_modification_tasks
5571            .insert(task_id.clone(), task);
5572        Ok(self.mac_sip_modification_tasks.get(&task_id).unwrap())
5573    }
5574
5575    // --- Declarative Policies Report operations ---
5576
5577    fn next_declarative_policies_report_id(&mut self) -> String {
5578        self.counters.declarative_policies_report += 1;
5579        format!("report-{:016x}", self.counters.declarative_policies_report)
5580    }
5581
5582    pub fn start_declarative_policies_report(
5583        &mut self,
5584        target_id: &str,
5585        s3_bucket: &str,
5586        s3_prefix: Option<String>,
5587        tags: Tags,
5588    ) -> &DeclarativePoliciesReport {
5589        let report_id = self.next_declarative_policies_report_id();
5590        let now = chrono::Utc::now()
5591            .format("%Y-%m-%dT%H:%M:%S.000Z")
5592            .to_string();
5593        let report = DeclarativePoliciesReport {
5594            report_id: report_id.clone(),
5595            s3_bucket: s3_bucket.to_string(),
5596            s3_prefix,
5597            target_id: target_id.to_string(),
5598            status: "running".to_string(),
5599            start_time: now,
5600            end_time: None,
5601            tags,
5602        };
5603        self.declarative_policies_reports
5604            .insert(report_id.clone(), report);
5605        self.declarative_policies_reports.get(&report_id).unwrap()
5606    }
5607
5608    pub fn cancel_declarative_policies_report(&mut self, report_id: &str) -> Result<(), Ec2Error> {
5609        let report = self
5610            .declarative_policies_reports
5611            .get_mut(report_id)
5612            .ok_or_else(|| Ec2Error::DeclarativePoliciesReportNotFound(report_id.to_string()))?;
5613        if report.status != "running" {
5614            return Err(Ec2Error::DeclarativePoliciesReportNotCancellable(
5615                report_id.to_string(),
5616            ));
5617        }
5618        let now = chrono::Utc::now()
5619            .format("%Y-%m-%dT%H:%M:%S.000Z")
5620            .to_string();
5621        report.status = "cancelled".to_string();
5622        report.end_time = Some(now);
5623        Ok(())
5624    }
5625
5626    // --- BYOIP CIDR operations ---
5627
5628    pub fn provision_byoip_cidr(
5629        &mut self,
5630        cidr: &str,
5631        description: Option<String>,
5632    ) -> Result<&ByoipCidr, Ec2Error> {
5633        if self.byoip_cidrs.contains_key(cidr) {
5634            return Err(Ec2Error::ByoipCidrAlreadyExists(cidr.to_string()));
5635        }
5636        let entry = ByoipCidr {
5637            cidr: cidr.to_string(),
5638            description,
5639            state: "provisioned".to_string(),
5640            asn_association: None,
5641            ipam_pool_id: None,
5642        };
5643        self.byoip_cidrs.insert(cidr.to_string(), entry);
5644        Ok(self.byoip_cidrs.get(cidr).unwrap())
5645    }
5646
5647    pub fn advertise_byoip_cidr(&mut self, cidr: &str) -> Result<&ByoipCidr, Ec2Error> {
5648        let entry = self
5649            .byoip_cidrs
5650            .get_mut(cidr)
5651            .ok_or_else(|| Ec2Error::InvalidByoipCidrNotFound(cidr.to_string()))?;
5652        if entry.state != "provisioned" && entry.state != "deprovisioned" {
5653            return Err(Ec2Error::ByoipCidrInvalidState(cidr.to_string()));
5654        }
5655        entry.state = "advertised".to_string();
5656        Ok(entry)
5657    }
5658
5659    pub fn withdraw_byoip_cidr(&mut self, cidr: &str) -> Result<&ByoipCidr, Ec2Error> {
5660        let entry = self
5661            .byoip_cidrs
5662            .get_mut(cidr)
5663            .ok_or_else(|| Ec2Error::InvalidByoipCidrNotFound(cidr.to_string()))?;
5664        if entry.state != "advertised" {
5665            return Err(Ec2Error::ByoipCidrInvalidState(cidr.to_string()));
5666        }
5667        entry.state = "provisioned".to_string();
5668        Ok(entry)
5669    }
5670
5671    pub fn deprovision_byoip_cidr(&mut self, cidr: &str) -> Result<&ByoipCidr, Ec2Error> {
5672        let entry = self
5673            .byoip_cidrs
5674            .get_mut(cidr)
5675            .ok_or_else(|| Ec2Error::InvalidByoipCidrNotFound(cidr.to_string()))?;
5676        if entry.state == "advertised" {
5677            return Err(Ec2Error::ByoipCidrInvalidState(cidr.to_string()));
5678        }
5679        entry.state = "deprovisioned".to_string();
5680        Ok(entry)
5681    }
5682
5683    // --- Public IPv4 pool operations ---
5684
5685    fn next_public_ipv4_pool_id(&mut self) -> String {
5686        self.counters.public_ipv4_pool += 1;
5687        format!("ipv4pool-ec2-{:08x}", self.counters.public_ipv4_pool)
5688    }
5689
5690    pub fn create_public_ipv4_pool(
5691        &mut self,
5692        description: Option<String>,
5693        network_border_group: Option<String>,
5694        tags: Tags,
5695    ) -> &PublicIpv4Pool {
5696        let pool_id = self.next_public_ipv4_pool_id();
5697        let pool = PublicIpv4Pool {
5698            pool_id: pool_id.clone(),
5699            description,
5700            network_border_group,
5701            total_address_count: 0,
5702            total_available_address_count: 0,
5703            pool_address_ranges: Vec::new(),
5704            tags,
5705        };
5706        self.public_ipv4_pools.insert(pool_id.clone(), pool);
5707        self.public_ipv4_pools.get(&pool_id).unwrap()
5708    }
5709
5710    pub fn delete_public_ipv4_pool(&mut self, pool_id: &str) -> Result<(), Ec2Error> {
5711        let pool = self
5712            .public_ipv4_pools
5713            .get(pool_id)
5714            .ok_or_else(|| Ec2Error::InvalidPublicIpv4PoolNotFound(pool_id.to_string()))?;
5715        if !pool.pool_address_ranges.is_empty() {
5716            return Err(Ec2Error::PublicIpv4PoolNotEmpty(pool_id.to_string()));
5717        }
5718        self.public_ipv4_pools.remove(pool_id);
5719        Ok(())
5720    }
5721
5722    pub fn provision_public_ipv4_pool_cidr(
5723        &mut self,
5724        pool_id: &str,
5725        cidr: &str,
5726        netmask_length: i32,
5727    ) -> Result<(String, PublicIpv4PoolRange), Ec2Error> {
5728        let pool = self
5729            .public_ipv4_pools
5730            .get_mut(pool_id)
5731            .ok_or_else(|| Ec2Error::InvalidPublicIpv4PoolNotFound(pool_id.to_string()))?;
5732        // For the mock we simply derive a synthetic first/last address pair
5733        // from the CIDR string and the requested netmask length. The real AWS
5734        // API allocates the addresses out of a previously-provisioned BYOIP
5735        // CIDR; here we just record what was asked for.
5736        let count = if (1..=32).contains(&netmask_length) {
5737            1i32 << ((32 - netmask_length).max(0) as u32)
5738        } else {
5739            1
5740        };
5741        let first = cidr.split('/').next().unwrap_or("0.0.0.0").to_string();
5742        // Synthesize a "last" address from the integer offset.
5743        let last_octet = count.saturating_sub(1).min(255) as u8;
5744        let mut octets: Vec<u8> = first
5745            .split('.')
5746            .filter_map(|p| p.parse().ok())
5747            .collect::<Vec<u8>>();
5748        if octets.len() == 4 {
5749            octets[3] = octets[3].saturating_add(last_octet);
5750        }
5751        let last = if octets.len() == 4 {
5752            format!("{}.{}.{}.{}", octets[0], octets[1], octets[2], octets[3])
5753        } else {
5754            first.clone()
5755        };
5756        let range = PublicIpv4PoolRange {
5757            first_address: first.clone(),
5758            last_address: last,
5759            address_count: count,
5760            available_address_count: count,
5761        };
5762        pool.pool_address_ranges.push(range.clone());
5763        pool.total_address_count += count;
5764        pool.total_available_address_count += count;
5765        Ok((first, range))
5766    }
5767
5768    pub fn deprovision_public_ipv4_pool_cidr(
5769        &mut self,
5770        pool_id: &str,
5771        cidr: &str,
5772    ) -> Result<Vec<String>, Ec2Error> {
5773        let pool = self
5774            .public_ipv4_pools
5775            .get_mut(pool_id)
5776            .ok_or_else(|| Ec2Error::InvalidPublicIpv4PoolNotFound(pool_id.to_string()))?;
5777        let first_addr = cidr.split('/').next().unwrap_or("").to_string();
5778        let pos = pool
5779            .pool_address_ranges
5780            .iter()
5781            .position(|r| r.first_address == first_addr)
5782            .ok_or_else(|| {
5783                Ec2Error::PublicIpv4PoolCidrNotFound(cidr.to_string(), pool_id.to_string())
5784            })?;
5785        let range = pool.pool_address_ranges.remove(pos);
5786        pool.total_address_count -= range.address_count;
5787        pool.total_available_address_count -= range.available_address_count;
5788        let mut deprovisioned = Vec::new();
5789        for i in 0..range.address_count {
5790            let octets: Vec<u8> = range
5791                .first_address
5792                .split('.')
5793                .filter_map(|p| p.parse().ok())
5794                .collect();
5795            if octets.len() == 4 {
5796                let new_last = octets[3].saturating_add(i as u8);
5797                deprovisioned.push(format!(
5798                    "{}.{}.{}.{}",
5799                    octets[0], octets[1], octets[2], new_last
5800                ));
5801            }
5802        }
5803        Ok(deprovisioned)
5804    }
5805
5806    // --- COIP CIDR operations ---
5807
5808    pub fn create_coip_cidr(
5809        &mut self,
5810        cidr: &str,
5811        coip_pool_id: &str,
5812    ) -> Result<&CoipCidr, Ec2Error> {
5813        if !self.coip_pools.contains_key(coip_pool_id) {
5814            return Err(Ec2Error::CoipPoolNotFound(coip_pool_id.to_string()));
5815        }
5816        let key = (cidr.to_string(), coip_pool_id.to_string());
5817        if self.coip_cidrs.contains_key(&key) {
5818            return Err(Ec2Error::CoipCidrAlreadyExists(
5819                cidr.to_string(),
5820                coip_pool_id.to_string(),
5821            ));
5822        }
5823        // Also reflect into the pool's `pool_cidrs` Vec so the existing
5824        // describe path can see the CIDR if it learns to render `coip_cidrs`.
5825        if let Some(pool) = self.coip_pools.get_mut(coip_pool_id)
5826            && !pool.pool_cidrs.iter().any(|c| c == cidr)
5827        {
5828            pool.pool_cidrs.push(cidr.to_string());
5829        }
5830        let cidr_entry = CoipCidr {
5831            cidr: cidr.to_string(),
5832            coip_pool_id: coip_pool_id.to_string(),
5833        };
5834        self.coip_cidrs.insert(key.clone(), cidr_entry);
5835        Ok(self.coip_cidrs.get(&key).unwrap())
5836    }
5837
5838    pub fn delete_coip_cidr(
5839        &mut self,
5840        cidr: &str,
5841        coip_pool_id: &str,
5842    ) -> Result<CoipCidr, Ec2Error> {
5843        let key = (cidr.to_string(), coip_pool_id.to_string());
5844        let entry = self.coip_cidrs.remove(&key).ok_or_else(|| {
5845            Ec2Error::CoipCidrNotFound(cidr.to_string(), coip_pool_id.to_string())
5846        })?;
5847        if let Some(pool) = self.coip_pools.get_mut(coip_pool_id) {
5848            pool.pool_cidrs.retain(|c| c != cidr);
5849        }
5850        Ok(entry)
5851    }
5852
5853    // --- Address transfer operations ---
5854
5855    pub fn accept_address_transfer(
5856        &mut self,
5857        allocation_id: &str,
5858    ) -> Result<&AddressTransfer, Ec2Error> {
5859        let entry = self
5860            .address_transfers
5861            .get_mut(allocation_id)
5862            .ok_or_else(|| Ec2Error::InvalidAddressTransferNotFound(allocation_id.to_string()))?;
5863        let now = chrono::Utc::now()
5864            .format("%Y-%m-%dT%H:%M:%S.000Z")
5865            .to_string();
5866        entry.address_transfer_status = "accepted".to_string();
5867        entry.transfer_offer_accepted_timestamp = Some(now);
5868        Ok(entry)
5869    }
5870
5871    // --- Address attribute operations ---
5872
5873    pub fn modify_address_attribute(
5874        &mut self,
5875        allocation_id: &str,
5876        domain_name: Option<String>,
5877    ) -> Result<&ElasticIp, Ec2Error> {
5878        let eip = self
5879            .elastic_ips
5880            .get_mut(allocation_id)
5881            .ok_or_else(|| Ec2Error::AllocationNotFound(allocation_id.to_string()))?;
5882        eip.address_attribute_ptr_record = domain_name;
5883        Ok(eip)
5884    }
5885
5886    pub fn reset_address_attribute(&mut self, allocation_id: &str) -> Result<&ElasticIp, Ec2Error> {
5887        let eip = self
5888            .elastic_ips
5889            .get_mut(allocation_id)
5890            .ok_or_else(|| Ec2Error::AllocationNotFound(allocation_id.to_string()))?;
5891        eip.address_attribute_ptr_record = None;
5892        Ok(eip)
5893    }
5894
5895    pub fn move_address_to_vpc(&mut self, public_ip: &str) -> Result<&ElasticIp, Ec2Error> {
5896        let eip = self
5897            .elastic_ips
5898            .values_mut()
5899            .find(|e| e.public_ip == public_ip)
5900            .ok_or_else(|| Ec2Error::AllocationNotFound(public_ip.to_string()))?;
5901        eip.domain = "vpc".to_string();
5902        Ok(eip)
5903    }
5904
5905    pub fn restore_address_to_classic(&mut self, public_ip: &str) -> Result<&ElasticIp, Ec2Error> {
5906        let eip = self
5907            .elastic_ips
5908            .values_mut()
5909            .find(|e| e.public_ip == public_ip)
5910            .ok_or_else(|| Ec2Error::AllocationNotFound(public_ip.to_string()))?;
5911        eip.domain = "standard".to_string();
5912        Ok(eip)
5913    }
5914
5915    // --- NAT gateway secondary address operations ---
5916
5917    fn next_nat_gw_assoc_id(&mut self) -> String {
5918        self.counters.nat_gateway_address_assoc += 1;
5919        format!(
5920            "eipassoc-natgw-{:08x}",
5921            self.counters.nat_gateway_address_assoc
5922        )
5923    }
5924
5925    pub fn assign_private_nat_gateway_address(
5926        &mut self,
5927        nat_gateway_id: &str,
5928        private_ips: Vec<String>,
5929    ) -> Result<&NatGateway, Ec2Error> {
5930        // Assign synthetic private IPs if none were specified — generate
5931        // sequential addresses inside a private range. Match AWS where the
5932        // operation accepts either explicit addresses or a count.
5933        let nat = self
5934            .nat_gateways
5935            .get_mut(nat_gateway_id)
5936            .ok_or_else(|| Ec2Error::NatGatewayNotFound(nat_gateway_id.to_string()))?;
5937        let mut next_octet = nat.secondary_addresses.len() as u8 + 10;
5938        let resolved: Vec<String> = if private_ips.is_empty() {
5939            // default: allocate one address
5940            let ip = format!("10.0.0.{next_octet}");
5941            next_octet = next_octet.saturating_add(1);
5942            vec![ip]
5943        } else {
5944            private_ips
5945        };
5946        let _ = next_octet; // silence unused
5947        for ip in resolved {
5948            nat.secondary_addresses.push(NatGatewayAddressAssociation {
5949                allocation_id: None,
5950                association_id: None,
5951                network_interface_id: Some(format!("eni-{nat_gateway_id}")),
5952                private_ip: Some(ip),
5953                public_ip: None,
5954                status: "succeeded".to_string(),
5955                is_primary: false,
5956            });
5957        }
5958        Ok(self.nat_gateways.get(nat_gateway_id).unwrap())
5959    }
5960
5961    pub fn unassign_private_nat_gateway_address(
5962        &mut self,
5963        nat_gateway_id: &str,
5964        private_ips: Vec<String>,
5965    ) -> Result<&NatGateway, Ec2Error> {
5966        let nat = self
5967            .nat_gateways
5968            .get_mut(nat_gateway_id)
5969            .ok_or_else(|| Ec2Error::NatGatewayNotFound(nat_gateway_id.to_string()))?;
5970        // Mark targeted addresses as unassigning, then drop them.
5971        let before = nat.secondary_addresses.len();
5972        nat.secondary_addresses.retain(|a| {
5973            !private_ips
5974                .iter()
5975                .any(|ip| a.private_ip.as_deref() == Some(ip))
5976        });
5977        if nat.secondary_addresses.len() == before && !private_ips.is_empty() {
5978            return Err(Ec2Error::InvalidNatGatewaySecondaryAddressNotFound(
5979                String::new(),
5980                private_ips.first().cloned().unwrap_or_default(),
5981            ));
5982        }
5983        Ok(self.nat_gateways.get(nat_gateway_id).unwrap())
5984    }
5985
5986    pub fn associate_nat_gateway_address(
5987        &mut self,
5988        nat_gateway_id: &str,
5989        allocation_ids: Vec<String>,
5990        private_ips: Vec<String>,
5991    ) -> Result<&NatGateway, Ec2Error> {
5992        // Resolve each allocation_id to its public IP up-front so we don't
5993        // borrow `self.elastic_ips` and `self.nat_gateways` simultaneously.
5994        let mut resolved: Vec<(String, Option<String>, Option<String>, String)> = Vec::new();
5995        for (idx, alloc) in allocation_ids.iter().enumerate() {
5996            let public_ip = self
5997                .elastic_ips
5998                .get(alloc)
5999                .map(|e| e.public_ip.clone())
6000                .ok_or_else(|| Ec2Error::AllocationNotFound(alloc.clone()))?;
6001            let private_ip = private_ips.get(idx).cloned();
6002            let assoc_id = self.next_nat_gw_assoc_id();
6003            resolved.push((alloc.clone(), Some(public_ip), private_ip, assoc_id));
6004        }
6005        let nat = self
6006            .nat_gateways
6007            .get_mut(nat_gateway_id)
6008            .ok_or_else(|| Ec2Error::NatGatewayNotFound(nat_gateway_id.to_string()))?;
6009        for (alloc, public_ip, private_ip, assoc_id) in resolved {
6010            nat.secondary_addresses.push(NatGatewayAddressAssociation {
6011                allocation_id: Some(alloc),
6012                association_id: Some(assoc_id),
6013                network_interface_id: Some(format!("eni-{nat_gateway_id}")),
6014                private_ip,
6015                public_ip,
6016                status: "succeeded".to_string(),
6017                is_primary: false,
6018            });
6019        }
6020        Ok(self.nat_gateways.get(nat_gateway_id).unwrap())
6021    }
6022
6023    pub fn disassociate_nat_gateway_address(
6024        &mut self,
6025        nat_gateway_id: &str,
6026        association_ids: Vec<String>,
6027    ) -> Result<&NatGateway, Ec2Error> {
6028        let nat = self
6029            .nat_gateways
6030            .get_mut(nat_gateway_id)
6031            .ok_or_else(|| Ec2Error::NatGatewayNotFound(nat_gateway_id.to_string()))?;
6032        let before = nat.secondary_addresses.len();
6033        nat.secondary_addresses.retain(|a| {
6034            !association_ids
6035                .iter()
6036                .any(|id| a.association_id.as_deref() == Some(id))
6037        });
6038        if nat.secondary_addresses.len() == before && !association_ids.is_empty() {
6039            return Err(Ec2Error::InvalidNatGatewaySecondaryAddressNotFound(
6040                association_ids.first().cloned().unwrap_or_default(),
6041                String::new(),
6042            ));
6043        }
6044        Ok(self.nat_gateways.get(nat_gateway_id).unwrap())
6045    }
6046
6047    // --- Group 4: ID generators ---
6048
6049    fn next_mac_volume_ownership_task_id(&mut self) -> String {
6050        self.counters.mac_volume_ownership_task += 1;
6051        format!(
6052            "macmodification-{:08x}",
6053            self.counters.mac_volume_ownership_task
6054        )
6055    }
6056
6057    fn next_replace_root_volume_task_id(&mut self) -> String {
6058        self.counters.replace_root_volume_task += 1;
6059        format!("replacevol-{:08x}", self.counters.replace_root_volume_task)
6060    }
6061
6062    fn next_snapshot_import_task_id(&mut self) -> String {
6063        self.counters.snapshot_import_task += 1;
6064        format!("import-snap-{:08x}", self.counters.snapshot_import_task)
6065    }
6066
6067    fn next_conversion_task_id(&mut self) -> String {
6068        self.counters.conversion_task += 1;
6069        format!("import-i-{:08x}", self.counters.conversion_task)
6070    }
6071
6072    fn next_export_task_id(&mut self) -> String {
6073        self.counters.export_task += 1;
6074        format!("export-i-{:08x}", self.counters.export_task)
6075    }
6076
6077    fn next_import_task_id(&mut self) -> String {
6078        self.counters.import_task += 1;
6079        format!("import-ami-{:08x}", self.counters.import_task)
6080    }
6081
6082    fn next_trunk_assoc_id(&mut self) -> String {
6083        self.counters.trunk_interface_assoc += 1;
6084        format!("trunk-assoc-{:08x}", self.counters.trunk_interface_assoc)
6085    }
6086
6087    fn next_secondary_network_id(&mut self) -> String {
6088        self.counters.secondary_network += 1;
6089        format!("snet-{:08x}", self.counters.secondary_network)
6090    }
6091
6092    fn next_secondary_subnet_id(&mut self) -> String {
6093        self.counters.secondary_subnet += 1;
6094        format!("ssubnet-{:08x}", self.counters.secondary_subnet)
6095    }
6096
6097    // --- Group 4: Mac Volume Ownership / Replace Root Volume ---
6098
6099    pub fn create_delegate_mac_volume_ownership_task(
6100        &mut self,
6101        instance_id: &str,
6102        source_account: &str,
6103        target_account: &str,
6104    ) -> &MacVolumeOwnershipTask {
6105        let task_id = self.next_mac_volume_ownership_task_id();
6106        let now = chrono::Utc::now()
6107            .format("%Y-%m-%dT%H:%M:%S.000Z")
6108            .to_string();
6109        let task = MacVolumeOwnershipTask {
6110            task_id: task_id.clone(),
6111            mac_volume_ownership_task_state: "pending".to_string(),
6112            volume_id: instance_id.to_string(),
6113            source_volume_owner_account_id: source_account.to_string(),
6114            target_volume_owner_account_id: target_account.to_string(),
6115            creation_time: now,
6116            completion_time: None,
6117        };
6118        self.mac_volume_ownership_tasks
6119            .insert(task_id.clone(), task);
6120        self.mac_volume_ownership_tasks.get(&task_id).unwrap()
6121    }
6122
6123    pub fn create_replace_root_volume_task(
6124        &mut self,
6125        instance_id: &str,
6126        image_id: Option<String>,
6127        snapshot_id: Option<String>,
6128        delete_replaced_root_volume: bool,
6129        tags: Tags,
6130    ) -> Result<&ReplaceRootVolumeTask, Ec2Error> {
6131        if !self.instances.contains_key(instance_id) {
6132            return Err(Ec2Error::InstanceNotFound(instance_id.to_string()));
6133        }
6134        let task_id = self.next_replace_root_volume_task_id();
6135        let now = chrono::Utc::now()
6136            .format("%Y-%m-%dT%H:%M:%S.000Z")
6137            .to_string();
6138        let task = ReplaceRootVolumeTask {
6139            task_id: task_id.clone(),
6140            instance_id: instance_id.to_string(),
6141            task_state: "pending".to_string(),
6142            image_id,
6143            snapshot_id,
6144            delete_replaced_root_volume,
6145            start_time: now,
6146            complete_time: None,
6147            tags,
6148        };
6149        self.replace_root_volume_tasks.insert(task_id.clone(), task);
6150        Ok(self.replace_root_volume_tasks.get(&task_id).unwrap())
6151    }
6152
6153    // --- Group 4: Snapshot Import ---
6154
6155    #[allow(clippy::too_many_arguments)]
6156    pub fn import_snapshot(
6157        &mut self,
6158        description: Option<String>,
6159        format: Option<String>,
6160        url: Option<String>,
6161        s3_bucket: Option<String>,
6162        s3_key: Option<String>,
6163        encrypted: bool,
6164        kms_key_id: Option<String>,
6165        owner_id: &str,
6166        tags: Tags,
6167    ) -> &SnapshotImportTask {
6168        let task_id = self.next_snapshot_import_task_id();
6169        // The mock "completes" the import immediately and materialises a
6170        // snapshot keyed off this import task.
6171        let snap_id = self.next_snapshot_id();
6172        let now = chrono::Utc::now()
6173            .format("%Y-%m-%dT%H:%M:%S.000Z")
6174            .to_string();
6175        let snap = Snapshot {
6176            snapshot_id: snap_id.clone(),
6177            volume_id: String::new(),
6178            volume_size: 8,
6179            state: "completed".to_string(),
6180            description: description.clone().unwrap_or_default(),
6181            start_time: now.clone(),
6182            progress: "100%".to_string(),
6183            owner_id: owner_id.to_string(),
6184            encrypted,
6185            tags: tags.clone(),
6186            lock_state: "none".to_string(),
6187            lock_duration: None,
6188            lock_created_on: None,
6189            lock_expires_on: None,
6190            lock_duration_start_time: None,
6191            cool_off_period: None,
6192            cool_off_period_expires_on: None,
6193            storage_tier: "standard".to_string(),
6194            last_tiering_operation_status: None,
6195            fast_snapshot_restore_states: Vec::new(),
6196        };
6197        self.snapshots.insert(snap_id.clone(), snap);
6198        let task = SnapshotImportTask {
6199            import_task_id: task_id.clone(),
6200            status: "completed".to_string(),
6201            description,
6202            disk_image_size: Some(8.0 * 1024.0 * 1024.0 * 1024.0),
6203            format,
6204            url,
6205            user_bucket_s3_bucket: s3_bucket,
6206            user_bucket_s3_key: s3_key,
6207            owner_id: owner_id.to_string(),
6208            encrypted,
6209            kms_key_id,
6210            snapshot_id: Some(snap_id),
6211            tags,
6212        };
6213        self.snapshot_import_tasks.insert(task_id.clone(), task);
6214        // Also register a generic import-task entry so CancelImportTask /
6215        // describe lookups can find it by import task ID.
6216        self.import_tasks
6217            .insert(task_id.clone(), ("completed".to_string(), None));
6218        self.snapshot_import_tasks.get(&task_id).unwrap()
6219    }
6220
6221    pub fn cancel_import_task(&mut self, task_id: &str) -> Result<(String, String), Ec2Error> {
6222        let entry = self
6223            .import_tasks
6224            .get_mut(task_id)
6225            .ok_or_else(|| Ec2Error::InvalidImportTaskNotFound(task_id.to_string()))?;
6226        let previous = entry.0.clone();
6227        entry.0 = "cancelled".to_string();
6228        entry.1 = Some(previous.clone());
6229        // Mirror the state into the snapshot import task if present so
6230        // describe operations stay consistent.
6231        if let Some(t) = self.snapshot_import_tasks.get_mut(task_id) {
6232            t.status = "cancelled".to_string();
6233        }
6234        Ok((previous, "cancelled".to_string()))
6235    }
6236
6237    // --- Group 4: Conversion / Export tasks ---
6238
6239    pub fn import_instance(
6240        &mut self,
6241        description: Option<String>,
6242        platform: &str,
6243        tags: Tags,
6244    ) -> &ConversionTask {
6245        let task_id = self.next_conversion_task_id();
6246        let now = chrono::Utc::now();
6247        let expires = (now + chrono::Duration::days(7))
6248            .format("%Y-%m-%dT%H:%M:%S.000Z")
6249            .to_string();
6250        let task = ConversionTask {
6251            conversion_task_id: task_id.clone(),
6252            expiration_time: expires,
6253            description,
6254            instance_id: None,
6255            platform: platform.to_string(),
6256            volumes: Vec::new(),
6257            state: "active".to_string(),
6258            status_message: None,
6259            tags,
6260        };
6261        self.conversion_tasks.insert(task_id.clone(), task);
6262        self.conversion_tasks.get(&task_id).unwrap()
6263    }
6264
6265    pub fn cancel_conversion_task(&mut self, task_id: &str) -> Result<(), Ec2Error> {
6266        // Try the regular conversion-task table first; fall back to the
6267        // ImportVolume task table.
6268        if let Some(task) = self.conversion_tasks.get_mut(task_id) {
6269            task.state = "cancelled".to_string();
6270            return Ok(());
6271        }
6272        if let Some(task) = self.import_volume_tasks.get_mut(task_id) {
6273            task.status = "cancelled".to_string();
6274            return Ok(());
6275        }
6276        Err(Ec2Error::InvalidConversionTaskNotFound(task_id.to_string()))
6277    }
6278
6279    #[allow(clippy::too_many_arguments)]
6280    pub fn create_instance_export_task(
6281        &mut self,
6282        description: String,
6283        instance_id: String,
6284        target_environment: String,
6285        disk_image_format: String,
6286        container_format: Option<String>,
6287        s3_bucket: String,
6288        s3_prefix: Option<String>,
6289        tags: Tags,
6290    ) -> &ExportTask {
6291        let task_id = self.next_export_task_id();
6292        let s3_key = format!(
6293            "{}{}/{}.{}",
6294            s3_prefix.clone().unwrap_or_default(),
6295            instance_id,
6296            task_id,
6297            match disk_image_format.as_str() {
6298                "vmdk" => "vmdk",
6299                "raw" => "raw",
6300                "vhd" => "vhd",
6301                _ => "vmdk",
6302            }
6303        );
6304        let task = ExportTask {
6305            export_task_id: task_id.clone(),
6306            description,
6307            instance_id,
6308            target_environment,
6309            disk_image_format,
6310            container_format,
6311            s3_bucket,
6312            s3_prefix,
6313            s3_key,
6314            status: "active".to_string(),
6315            status_message: None,
6316            tags,
6317        };
6318        self.export_tasks.insert(task_id.clone(), task);
6319        self.export_tasks.get(&task_id).unwrap()
6320    }
6321
6322    pub fn cancel_export_task(&mut self, task_id: &str) -> Result<(), Ec2Error> {
6323        let task = self
6324            .export_tasks
6325            .get_mut(task_id)
6326            .ok_or_else(|| Ec2Error::InvalidExportTaskNotFound(task_id.to_string()))?;
6327        task.status = "cancelled".to_string();
6328        Ok(())
6329    }
6330
6331    // --- Group 4: Trunk interface ---
6332
6333    #[allow(clippy::too_many_arguments)]
6334    pub fn associate_trunk_interface(
6335        &mut self,
6336        branch_interface_id: String,
6337        trunk_interface_id: String,
6338        interface_protocol: String,
6339        vlan_id: Option<i32>,
6340        gre_key: Option<i32>,
6341        tags: Tags,
6342    ) -> &TrunkInterfaceAssociation {
6343        let assoc_id = self.next_trunk_assoc_id();
6344        let assoc = TrunkInterfaceAssociation {
6345            association_id: assoc_id.clone(),
6346            branch_interface_id,
6347            trunk_interface_id,
6348            interface_protocol,
6349            vlan_id,
6350            gre_key,
6351            tags,
6352        };
6353        self.trunk_interface_associations
6354            .insert(assoc_id.clone(), assoc);
6355        self.trunk_interface_associations.get(&assoc_id).unwrap()
6356    }
6357
6358    pub fn disassociate_trunk_interface(&mut self, assoc_id: &str) -> Result<(), Ec2Error> {
6359        if self.trunk_interface_associations.remove(assoc_id).is_none() {
6360            return Err(Ec2Error::InvalidTrunkInterfaceAssociationNotFound(
6361                assoc_id.to_string(),
6362            ));
6363        }
6364        Ok(())
6365    }
6366
6367    // --- Group 4: Secondary network / subnet ---
6368
6369    pub fn create_secondary_network(
6370        &mut self,
6371        vpc_id: String,
6372        primary_cidr_block: String,
6373        network_border_group: Option<String>,
6374        tags: Tags,
6375    ) -> &SecondaryNetwork {
6376        let id = self.next_secondary_network_id();
6377        let network = SecondaryNetwork {
6378            network_id: id.clone(),
6379            vpc_id,
6380            primary_cidr_block,
6381            secondary_cidr_blocks: Vec::new(),
6382            state: "available".to_string(),
6383            network_border_group,
6384            tags,
6385        };
6386        self.secondary_networks.insert(id.clone(), network);
6387        self.secondary_networks.get(&id).unwrap()
6388    }
6389
6390    pub fn delete_secondary_network(&mut self, id: &str) -> Result<SecondaryNetwork, Ec2Error> {
6391        if !self.secondary_networks.contains_key(id) {
6392            return Err(Ec2Error::InvalidSecondaryNetworkNotFound(id.to_string()));
6393        }
6394        let has_subnets = self
6395            .secondary_subnets
6396            .values()
6397            .any(|s| s.secondary_network_id == id);
6398        if has_subnets {
6399            return Err(Ec2Error::SecondaryNetworkHasSubnets(id.to_string()));
6400        }
6401        let mut removed = self.secondary_networks.remove(id).unwrap();
6402        removed.state = "deleted".to_string();
6403        Ok(removed)
6404    }
6405
6406    pub fn create_secondary_subnet(
6407        &mut self,
6408        secondary_network_id: String,
6409        cidr_block: String,
6410        availability_zone: String,
6411        tags: Tags,
6412    ) -> Result<&SecondarySubnet, Ec2Error> {
6413        let net = self
6414            .secondary_networks
6415            .get(&secondary_network_id)
6416            .ok_or_else(|| {
6417                Ec2Error::InvalidSecondaryNetworkNotFound(secondary_network_id.clone())
6418            })?;
6419        let vpc_id = net.vpc_id.clone();
6420        let id = self.next_secondary_subnet_id();
6421        let subnet = SecondarySubnet {
6422            subnet_id: id.clone(),
6423            vpc_id,
6424            secondary_network_id,
6425            cidr_block,
6426            availability_zone,
6427            state: "available".to_string(),
6428            tags,
6429        };
6430        self.secondary_subnets.insert(id.clone(), subnet);
6431        Ok(self.secondary_subnets.get(&id).unwrap())
6432    }
6433
6434    pub fn delete_secondary_subnet(&mut self, id: &str) -> Result<SecondarySubnet, Ec2Error> {
6435        let mut removed = self
6436            .secondary_subnets
6437            .remove(id)
6438            .ok_or_else(|| Ec2Error::InvalidSecondarySubnetNotFound(id.to_string()))?;
6439        removed.state = "deleted".to_string();
6440        Ok(removed)
6441    }
6442
6443    // ===== Group 5 helpers =====
6444
6445    fn now_iso(&self) -> String {
6446        chrono::Utc::now()
6447            .format("%Y-%m-%dT%H:%M:%S.000Z")
6448            .to_string()
6449    }
6450
6451    fn next_reserved_instances_exchange_id(&mut self) -> String {
6452        self.counters.reserved_instances_exchange += 1;
6453        format!("riex-{:08x}", self.counters.reserved_instances_exchange)
6454    }
6455
6456    fn next_reserved_instances_listing_id(&mut self) -> String {
6457        self.counters.reserved_instances_listing += 1;
6458        format!("ril-{:08x}", self.counters.reserved_instances_listing)
6459    }
6460
6461    fn next_reserved_instances_purchase_id(&mut self) -> String {
6462        self.counters.reserved_instances_purchase += 1;
6463        format!("rip-{:08x}", self.counters.reserved_instances_purchase)
6464    }
6465
6466    fn next_reserved_instances_id(&mut self) -> String {
6467        self.counters.reserved_instances += 1;
6468        format!("ri-{:08x}", self.counters.reserved_instances)
6469    }
6470
6471    fn next_reserved_instances_modification_id(&mut self) -> String {
6472        self.counters.reserved_instances_modification += 1;
6473        format!("rim-{:08x}", self.counters.reserved_instances_modification)
6474    }
6475
6476    fn next_fpga_image_id(&mut self) -> String {
6477        self.counters.fpga_image += 1;
6478        format!("afi-{:08x}", self.counters.fpga_image)
6479    }
6480
6481    fn next_image_usage_report_id(&mut self) -> String {
6482        self.counters.image_usage_report += 1;
6483        format!("iur-{:08x}", self.counters.image_usage_report)
6484    }
6485
6486    fn next_import_image_task_id(&mut self) -> String {
6487        self.counters.import_image_task += 1;
6488        format!("import-ami-{:08x}", self.counters.import_image_task)
6489    }
6490
6491    fn next_instance_event_window_id(&mut self) -> String {
6492        self.counters.instance_event_window += 1;
6493        format!("iew-{:08x}", self.counters.instance_event_window)
6494    }
6495
6496    fn next_instance_event_id(&mut self) -> String {
6497        self.counters.instance_event += 1;
6498        format!("event-{:08x}", self.counters.instance_event)
6499    }
6500
6501    fn next_host_reservation_id(&mut self) -> String {
6502        self.counters.host_reservation += 1;
6503        format!("hr-{:08x}", self.counters.host_reservation)
6504    }
6505
6506    fn next_scheduled_instance_id(&mut self) -> String {
6507        self.counters.scheduled_instance += 1;
6508        format!("sci-{:08x}", self.counters.scheduled_instance)
6509    }
6510
6511    pub fn next_instance_id_pub(&mut self) -> String {
6512        self.counters.instance += 1;
6513        format!("i-{:08x}", self.counters.instance)
6514    }
6515
6516    // ----- Reserved Instances family -----
6517
6518    pub fn accept_reserved_instances_exchange_quote(
6519        &mut self,
6520        target_reserved_instances_ids: Vec<String>,
6521        source_reserved_instances_ids: Vec<String>,
6522    ) -> String {
6523        let id = self.next_reserved_instances_exchange_id();
6524        let now = self.now_iso();
6525        self.reserved_instances_exchanges.insert(
6526            id.clone(),
6527            ReservedInstancesExchange {
6528                exchange_id: id.clone(),
6529                target_reserved_instances_ids,
6530                source_reserved_instances_ids,
6531                status: "completed".to_string(),
6532                status_message: None,
6533                time: now,
6534            },
6535        );
6536        id
6537    }
6538
6539    pub fn cancel_reserved_instances_listing(
6540        &mut self,
6541        listing_id: &str,
6542    ) -> Result<&ReservedInstancesListing, Ec2Error> {
6543        let listing = self
6544            .reserved_instances_listings
6545            .get_mut(listing_id)
6546            .ok_or_else(|| {
6547                Ec2Error::InvalidReservedInstancesListingNotFound(listing_id.to_string())
6548            })?;
6549        listing.status = "cancelled".to_string();
6550        listing.update_date = chrono::Utc::now()
6551            .format("%Y-%m-%dT%H:%M:%S.000Z")
6552            .to_string();
6553        Ok(self.reserved_instances_listings.get(listing_id).unwrap())
6554    }
6555
6556    pub fn create_reserved_instances_listing(
6557        &mut self,
6558        reserved_instances_id: &str,
6559        instance_count: i32,
6560        price_schedules: Vec<PriceSchedule>,
6561        client_token: Option<String>,
6562        tags: Tags,
6563    ) -> &ReservedInstancesListing {
6564        let id = self.next_reserved_instances_listing_id();
6565        let now = self.now_iso();
6566        let listing = ReservedInstancesListing {
6567            listing_id: id.clone(),
6568            reserved_instances_id: reserved_instances_id.to_string(),
6569            instance_count,
6570            price_schedules,
6571            status: "active".to_string(),
6572            status_message: None,
6573            create_date: now.clone(),
6574            update_date: now,
6575            client_token,
6576            tags,
6577        };
6578        self.reserved_instances_listings.insert(id.clone(), listing);
6579        self.reserved_instances_listings.get(&id).unwrap()
6580    }
6581
6582    pub fn delete_queued_reserved_instances(
6583        &mut self,
6584        ids: &[String],
6585    ) -> (Vec<String>, Vec<String>) {
6586        let mut succ = Vec::new();
6587        let mut fail = Vec::new();
6588        for id in ids {
6589            if self
6590                .queued_reserved_instances_purchases
6591                .remove(id)
6592                .is_some()
6593            {
6594                succ.push(id.clone());
6595            } else {
6596                fail.push(id.clone());
6597            }
6598        }
6599        (succ, fail)
6600    }
6601
6602    pub fn modify_fleet(
6603        &mut self,
6604        fleet_id: &str,
6605        total_target_capacity: Option<i32>,
6606        on_demand_target_capacity: Option<i32>,
6607        spot_target_capacity: Option<i32>,
6608        context: Option<String>,
6609    ) -> Result<(), Ec2Error> {
6610        let fleet = self.ec2_fleets.get_mut(fleet_id).ok_or_else(|| {
6611            Ec2Error::InvalidParameterValue(format!("InvalidFleetId: {fleet_id}"))
6612        })?;
6613        if let Some(v) = total_target_capacity {
6614            fleet.total_target_capacity = Some(v);
6615        }
6616        if let Some(v) = on_demand_target_capacity {
6617            fleet.on_demand_target_capacity = Some(v);
6618        }
6619        if let Some(v) = spot_target_capacity {
6620            fleet.spot_target_capacity = Some(v);
6621        }
6622        if let Some(v) = context {
6623            fleet.context = Some(v);
6624        }
6625        Ok(())
6626    }
6627
6628    pub fn modify_reserved_instances(
6629        &mut self,
6630        reserved_instances_ids: Vec<String>,
6631        target_configurations: Vec<ReservedInstancesConfiguration>,
6632        client_token: Option<String>,
6633    ) -> String {
6634        let id = self.next_reserved_instances_modification_id();
6635        let now = self.now_iso();
6636        let modification = ReservedInstancesModification {
6637            modification_id: id.clone(),
6638            reserved_instances_ids,
6639            target_configurations,
6640            status: "fulfilled".to_string(),
6641            status_message: None,
6642            create_date: now.clone(),
6643            update_date: now.clone(),
6644            effective_date: now,
6645            client_token,
6646        };
6647        self.reserved_instances_modifications
6648            .insert(id.clone(), modification);
6649        id
6650    }
6651
6652    pub fn purchase_reserved_instances_offering(
6653        &mut self,
6654        offering_id: &str,
6655        instance_count: i32,
6656        limit_price: Option<String>,
6657        tags: Tags,
6658    ) -> String {
6659        let now = self.now_iso();
6660        let ri_id = self.next_reserved_instances_id();
6661        let purchase_id = self.next_reserved_instances_purchase_id();
6662        let ri = ReservedInstances {
6663            reserved_instances_id: ri_id.clone(),
6664            instance_type: "t3.micro".to_string(),
6665            instance_count,
6666            product_description: "Linux/UNIX".to_string(),
6667            scope: "Region".to_string(),
6668            currency_code: "USD".to_string(),
6669            duration: 31536000,
6670            fixed_price: 0.0,
6671            usage_price: 0.0,
6672            offering_class: "standard".to_string(),
6673            offering_type: "No Upfront".to_string(),
6674            instance_tenancy: "default".to_string(),
6675            start: now.clone(),
6676            end: now.clone(),
6677            state: "active".to_string(),
6678            tags: tags.clone(),
6679        };
6680        self.reserved_instances.insert(ri_id.clone(), ri);
6681        let purchase = ReservedInstancesPurchase {
6682            purchase_id: purchase_id.clone(),
6683            reserved_instances_offering_id: offering_id.to_string(),
6684            instance_count,
6685            limit_price,
6686            purchase_time: now,
6687            tags,
6688            queued: false,
6689            reserved_instances_id: Some(ri_id.clone()),
6690        };
6691        self.reserved_instances_purchases
6692            .insert(purchase_id, purchase);
6693        ri_id
6694    }
6695
6696    // ----- Image / FPGA Image family -----
6697
6698    pub fn cancel_image_launch_permission(&mut self, image_id: &str) -> Result<(), Ec2Error> {
6699        let img = self
6700            .images
6701            .get_mut(image_id)
6702            .ok_or_else(|| Ec2Error::AmiNotFound(image_id.to_string()))?;
6703        img.launch_permissions.clear();
6704        Ok(())
6705    }
6706
6707    pub fn create_fpga_image(
6708        &mut self,
6709        name: Option<String>,
6710        description: Option<String>,
6711        owner_id: &str,
6712        tags: Tags,
6713    ) -> &FpgaImage {
6714        let id = self.next_fpga_image_id();
6715        let global_id = format!("agfi-{:08x}", self.counters.fpga_image);
6716        let now = self.now_iso();
6717        let img = FpgaImage {
6718            fpga_image_id: id.clone(),
6719            fpga_image_global_id: global_id,
6720            name: name.unwrap_or_else(|| format!("fpga-{}", self.counters.fpga_image)),
6721            description,
6722            shell_version: Some("0x04261818".to_string()),
6723            pci_id_vendor: None,
6724            pci_id_device: None,
6725            state: "available".to_string(),
6726            create_time: now.clone(),
6727            update_time: now,
6728            owner_id: owner_id.to_string(),
6729            owner_alias: None,
6730            product_codes: Vec::new(),
6731            tags,
6732            public: false,
6733            data_retention_support: false,
6734            instance_types: Vec::new(),
6735            load_permissions: Vec::new(),
6736        };
6737        self.fpga_images.insert(id.clone(), img);
6738        self.fpga_images.get(&id).unwrap()
6739    }
6740
6741    pub fn copy_fpga_image(
6742        &mut self,
6743        source_fpga_image_id: &str,
6744        name: Option<String>,
6745        description: Option<String>,
6746        owner_id: &str,
6747    ) -> Result<String, Ec2Error> {
6748        let src = self
6749            .fpga_images
6750            .get(source_fpga_image_id)
6751            .ok_or_else(|| Ec2Error::InvalidFpgaImageNotFound(source_fpga_image_id.to_string()))?
6752            .clone();
6753        let id = self.next_fpga_image_id();
6754        let global_id = format!("agfi-{:08x}", self.counters.fpga_image);
6755        let now = self.now_iso();
6756        let img = FpgaImage {
6757            fpga_image_id: id.clone(),
6758            fpga_image_global_id: global_id,
6759            name: name.unwrap_or(src.name),
6760            description: description.or(src.description),
6761            shell_version: src.shell_version,
6762            pci_id_vendor: src.pci_id_vendor,
6763            pci_id_device: src.pci_id_device,
6764            state: "available".to_string(),
6765            create_time: now.clone(),
6766            update_time: now,
6767            owner_id: owner_id.to_string(),
6768            owner_alias: None,
6769            product_codes: Vec::new(),
6770            tags: Tags::new(),
6771            public: false,
6772            data_retention_support: src.data_retention_support,
6773            instance_types: src.instance_types,
6774            load_permissions: Vec::new(),
6775        };
6776        self.fpga_images.insert(id.clone(), img);
6777        Ok(id)
6778    }
6779
6780    pub fn delete_fpga_image(&mut self, id: &str) -> Result<(), Ec2Error> {
6781        self.fpga_images
6782            .remove(id)
6783            .ok_or_else(|| Ec2Error::InvalidFpgaImageNotFound(id.to_string()))?;
6784        Ok(())
6785    }
6786
6787    pub fn modify_fpga_image_attribute(
6788        &mut self,
6789        id: &str,
6790        name: Option<String>,
6791        description: Option<String>,
6792        add_load: Vec<(String, String)>,
6793        remove_load: Vec<(String, String)>,
6794    ) -> Result<&FpgaImage, Ec2Error> {
6795        let img = self
6796            .fpga_images
6797            .get_mut(id)
6798            .ok_or_else(|| Ec2Error::InvalidFpgaImageNotFound(id.to_string()))?;
6799        if let Some(n) = name {
6800            img.name = n;
6801        }
6802        if let Some(d) = description {
6803            img.description = Some(d);
6804        }
6805        for r in &remove_load {
6806            img.load_permissions.retain(|p| p != r);
6807        }
6808        for a in add_load {
6809            if !img.load_permissions.contains(&a) {
6810                img.load_permissions.push(a);
6811            }
6812        }
6813        Ok(self.fpga_images.get(id).unwrap())
6814    }
6815
6816    pub fn reset_fpga_image_attribute(&mut self, id: &str, attr: &str) -> Result<(), Ec2Error> {
6817        let img = self
6818            .fpga_images
6819            .get_mut(id)
6820            .ok_or_else(|| Ec2Error::InvalidFpgaImageNotFound(id.to_string()))?;
6821        match attr {
6822            "loadPermission" => img.load_permissions.clear(),
6823            "description" => img.description = None,
6824            _ => {}
6825        }
6826        Ok(())
6827    }
6828
6829    pub fn create_image_usage_report(
6830        &mut self,
6831        image_id: &str,
6832        account_filters: Vec<String>,
6833        resource_types: Vec<String>,
6834        tags: Tags,
6835    ) -> Result<String, Ec2Error> {
6836        if !self.images.contains_key(image_id) {
6837            return Err(Ec2Error::AmiNotFound(image_id.to_string()));
6838        }
6839        let id = self.next_image_usage_report_id();
6840        let now = self.now_iso();
6841        self.image_usage_reports.insert(
6842            id.clone(),
6843            ImageUsageReport {
6844                report_id: id.clone(),
6845                image_id: image_id.to_string(),
6846                account_filters,
6847                resource_types,
6848                status: "completed".to_string(),
6849                creation_time: now.clone(),
6850                completion_time: Some(now),
6851                tags,
6852            },
6853        );
6854        Ok(id)
6855    }
6856
6857    pub fn delete_image_usage_report(&mut self, id: &str) -> Result<(), Ec2Error> {
6858        self.image_usage_reports
6859            .remove(id)
6860            .ok_or_else(|| Ec2Error::InvalidImageUsageReportNotFound(id.to_string()))?;
6861        Ok(())
6862    }
6863
6864    pub fn create_restore_image_task(
6865        &mut self,
6866        bucket: &str,
6867        object_key: &str,
6868        name: Option<String>,
6869        owner_id: &str,
6870    ) -> String {
6871        let image_id = self.next_ami_id();
6872        let now = self.now_iso();
6873        let img_name = name.clone().unwrap_or_else(|| image_id.clone());
6874        // Materialise an Image so subsequent DescribeImages calls succeed.
6875        self.images.insert(
6876            image_id.clone(),
6877            Image {
6878                image_id: image_id.clone(),
6879                name: img_name.clone(),
6880                description: format!("Restored from s3://{bucket}/{object_key}"),
6881                state: "available".to_string(),
6882                owner_id: owner_id.to_string(),
6883                architecture: "x86_64".to_string(),
6884                image_type: "machine".to_string(),
6885                platform: None,
6886                virtualization_type: "hvm".to_string(),
6887                root_device_type: "ebs".to_string(),
6888                root_device_name: "/dev/xvda".to_string(),
6889                public: false,
6890                tags: Tags::new(),
6891                source_instance_id: None,
6892                source_instance_type: String::new(),
6893                launch_permissions: Vec::new(),
6894                recycle_bin_state: None,
6895                deprecation_time: None,
6896                recycle_bin_enter_time: None,
6897                product_codes: Vec::new(),
6898                fast_launch_state: None,
6899                deregistration_protection: None,
6900                kernel_id: None,
6901                ramdisk_id: None,
6902                ena_support: None,
6903                sriov_net_support: None,
6904                tpm_support: None,
6905                boot_mode: None,
6906                imds_support: None,
6907                image_location: Some(format!("s3://{bucket}/{object_key}")),
6908                source_image_id: None,
6909                source_region: None,
6910            },
6911        );
6912        self.restore_image_tasks.insert(
6913            image_id.clone(),
6914            RestoreImageTask {
6915                image_id: image_id.clone(),
6916                name: img_name,
6917                s3_object_url: format!("s3://{bucket}/{object_key}"),
6918                status: "completed".to_string(),
6919                status_message: None,
6920                creation_time: now,
6921            },
6922        );
6923        image_id
6924    }
6925
6926    pub fn create_store_image_task(
6927        &mut self,
6928        ami_id: &str,
6929        bucket: &str,
6930    ) -> Result<String, Ec2Error> {
6931        if !self.images.contains_key(ami_id) {
6932            return Err(Ec2Error::AmiNotFound(ami_id.to_string()));
6933        }
6934        let object_key = format!("{ami_id}.bin");
6935        let now = self.now_iso();
6936        self.store_image_tasks.insert(
6937            ami_id.to_string(),
6938            StoreImageTask {
6939                image_id: ami_id.to_string(),
6940                ami_id: ami_id.to_string(),
6941                bucket: bucket.to_string(),
6942                s3_object_key: object_key.clone(),
6943                store_task_state: "completed".to_string(),
6944                store_task_failure_reason: None,
6945                progress_percentage: 100,
6946                task_start_time: now,
6947            },
6948        );
6949        Ok(object_key)
6950    }
6951
6952    pub fn import_image(
6953        &mut self,
6954        description: Option<String>,
6955        license_type: Option<String>,
6956        platform: Option<String>,
6957        architecture: Option<String>,
6958        encrypted: bool,
6959        tags: Tags,
6960    ) -> &ImportImageTask {
6961        let id = self.next_import_image_task_id();
6962        let task = ImportImageTask {
6963            import_task_id: id.clone(),
6964            architecture,
6965            description,
6966            encrypted,
6967            hypervisor: Some("xen".to_string()),
6968            image_id: None,
6969            license_type,
6970            platform,
6971            progress: Some("0".to_string()),
6972            snapshot_details: Vec::new(),
6973            status: "active".to_string(),
6974            status_message: Some("pending".to_string()),
6975            tags,
6976            usage_operation: None,
6977            boot_mode: Some("uefi".to_string()),
6978        };
6979        self.import_image_tasks.insert(id.clone(), task);
6980        self.import_image_tasks.get(&id).unwrap()
6981    }
6982
6983    pub fn restore_image_from_recycle_bin(&mut self, image_id: &str) -> Result<(), Ec2Error> {
6984        let img = self
6985            .images
6986            .get_mut(image_id)
6987            .ok_or_else(|| Ec2Error::AmiNotFound(image_id.to_string()))?;
6988        if img.recycle_bin_state.as_deref() != Some("recycled") {
6989            return Err(Ec2Error::ImageNotInRecycleBin(image_id.to_string()));
6990        }
6991        img.recycle_bin_state = None;
6992        img.state = "available".to_string();
6993        self.deregistered_images.remove(image_id);
6994        Ok(())
6995    }
6996
6997    pub fn replace_allowed_image_criteria(&mut self, criteria: Vec<AllowedImageCriterion>) {
6998        self.allowed_image_criteria = criteria;
6999    }
7000
7001    // ----- Instance modify family -----
7002
7003    pub fn modify_default_credit_specification(
7004        &mut self,
7005        instance_family: &str,
7006        cpu_credits: &str,
7007    ) -> InstanceFamilyCreditPair {
7008        self.default_credit_specifications
7009            .insert(instance_family.to_string(), cpu_credits.to_string());
7010        InstanceFamilyCreditPair {
7011            instance_family: instance_family.to_string(),
7012            cpu_credits: cpu_credits.to_string(),
7013        }
7014    }
7015
7016    pub fn modify_instance_connect_endpoint(
7017        &mut self,
7018        endpoint_id: &str,
7019        preserve_client_ip: Option<bool>,
7020        security_group_ids: Option<Vec<String>>,
7021    ) -> Result<(), Ec2Error> {
7022        let ep = self
7023            .instance_connect_endpoints
7024            .get_mut(endpoint_id)
7025            .ok_or_else(|| Ec2Error::InstanceConnectEndpointNotFound(endpoint_id.to_string()))?;
7026        if let Some(v) = preserve_client_ip {
7027            ep.preserve_client_ip = v;
7028        }
7029        if let Some(v) = security_group_ids {
7030            ep.security_group_ids = v;
7031        }
7032        Ok(())
7033    }
7034
7035    pub fn modify_instance_cpu_options(
7036        &mut self,
7037        instance_id: &str,
7038        core_count: Option<i32>,
7039        threads_per_core: Option<i32>,
7040    ) -> Result<&Instance, Ec2Error> {
7041        let inst = self
7042            .instances
7043            .get_mut(instance_id)
7044            .ok_or_else(|| Ec2Error::InstanceNotFound(instance_id.to_string()))?;
7045        let mut opts = inst.cpu_options.clone().unwrap_or_default();
7046        if let Some(v) = core_count {
7047            opts.core_count = Some(v);
7048        }
7049        if let Some(v) = threads_per_core {
7050            opts.threads_per_core = Some(v);
7051        }
7052        inst.cpu_options = Some(opts);
7053        Ok(self.instances.get(instance_id).unwrap())
7054    }
7055
7056    pub fn modify_instance_credit_specification(
7057        &mut self,
7058        specs: &[(String, String)],
7059    ) -> (Vec<String>, Vec<String>) {
7060        let mut succ = Vec::new();
7061        let mut fail = Vec::new();
7062        for (instance_id, cpu_credits) in specs {
7063            if let Some(inst) = self.instances.get_mut(instance_id) {
7064                inst.credit_specification = Some(cpu_credits.clone());
7065                succ.push(instance_id.clone());
7066            } else {
7067                fail.push(instance_id.clone());
7068            }
7069        }
7070        (succ, fail)
7071    }
7072
7073    pub fn modify_instance_maintenance_options(
7074        &mut self,
7075        instance_id: &str,
7076        auto_recovery: Option<String>,
7077        reboot_migration: Option<String>,
7078    ) -> Result<&Instance, Ec2Error> {
7079        let inst = self
7080            .instances
7081            .get_mut(instance_id)
7082            .ok_or_else(|| Ec2Error::InstanceNotFound(instance_id.to_string()))?;
7083        let mut opts = inst.maintenance_options.clone().unwrap_or_default();
7084        if let Some(v) = auto_recovery {
7085            opts.auto_recovery = Some(v);
7086        }
7087        if let Some(v) = reboot_migration {
7088            opts.reboot_migration = Some(v);
7089        }
7090        inst.maintenance_options = Some(opts);
7091        Ok(self.instances.get(instance_id).unwrap())
7092    }
7093
7094    pub fn modify_instance_metadata_defaults(
7095        &mut self,
7096        http_tokens: Option<String>,
7097        http_put_response_hop_limit: Option<i32>,
7098        http_endpoint: Option<String>,
7099        instance_metadata_tags: Option<String>,
7100    ) {
7101        let mut defaults = self.instance_metadata_defaults.clone().unwrap_or_default();
7102        if let Some(v) = http_tokens {
7103            defaults.http_tokens = Some(v);
7104        }
7105        if let Some(v) = http_put_response_hop_limit {
7106            defaults.http_put_response_hop_limit = Some(v);
7107        }
7108        if let Some(v) = http_endpoint {
7109            defaults.http_endpoint = Some(v);
7110        }
7111        if let Some(v) = instance_metadata_tags {
7112            defaults.instance_metadata_tags = Some(v);
7113        }
7114        self.instance_metadata_defaults = Some(defaults);
7115    }
7116
7117    pub fn modify_instance_network_performance_options(
7118        &mut self,
7119        instance_id: &str,
7120        bandwidth_weighting: &str,
7121    ) -> Result<(), Ec2Error> {
7122        let inst = self
7123            .instances
7124            .get_mut(instance_id)
7125            .ok_or_else(|| Ec2Error::InstanceNotFound(instance_id.to_string()))?;
7126        inst.network_bandwidth_weighting = Some(bandwidth_weighting.to_string());
7127        Ok(())
7128    }
7129
7130    pub fn reset_instance_attribute(&mut self, instance_id: &str) -> Result<(), Ec2Error> {
7131        if !self.instances.contains_key(instance_id) {
7132            return Err(Ec2Error::InstanceNotFound(instance_id.to_string()));
7133        }
7134        Ok(())
7135    }
7136
7137    // ----- Instance event window family -----
7138
7139    pub fn create_instance_event_window(
7140        &mut self,
7141        name: Option<String>,
7142        time_ranges: Vec<InstanceEventWindowTimeRange>,
7143        cron_expression: Option<String>,
7144        tags: Tags,
7145    ) -> &InstanceEventWindow {
7146        let id = self.next_instance_event_window_id();
7147        let win = InstanceEventWindow {
7148            instance_event_window_id: id.clone(),
7149            name: name
7150                .unwrap_or_else(|| format!("event-window-{}", self.counters.instance_event_window)),
7151            time_ranges,
7152            cron_expression,
7153            association_target: None,
7154            state: "active".to_string(),
7155            tags,
7156        };
7157        self.instance_event_windows.insert(id.clone(), win);
7158        self.instance_event_windows.get(&id).unwrap()
7159    }
7160
7161    pub fn modify_instance_event_window(
7162        &mut self,
7163        id: &str,
7164        name: Option<String>,
7165        time_ranges: Option<Vec<InstanceEventWindowTimeRange>>,
7166        cron_expression: Option<String>,
7167    ) -> Result<&InstanceEventWindow, Ec2Error> {
7168        let win = self
7169            .instance_event_windows
7170            .get_mut(id)
7171            .ok_or_else(|| Ec2Error::InvalidInstanceEventWindowNotFound(id.to_string()))?;
7172        if let Some(n) = name {
7173            win.name = n;
7174        }
7175        if let Some(t) = time_ranges {
7176            win.time_ranges = t;
7177        }
7178        if let Some(c) = cron_expression {
7179            win.cron_expression = Some(c);
7180        }
7181        Ok(self.instance_event_windows.get(id).unwrap())
7182    }
7183
7184    pub fn delete_instance_event_window(&mut self, id: &str) -> Result<(), Ec2Error> {
7185        self.instance_event_windows
7186            .remove(id)
7187            .ok_or_else(|| Ec2Error::InvalidInstanceEventWindowNotFound(id.to_string()))?;
7188        Ok(())
7189    }
7190
7191    pub fn associate_instance_event_window(
7192        &mut self,
7193        id: &str,
7194        instance_ids: Vec<String>,
7195        dedicated_host_ids: Vec<String>,
7196        tags: Vec<(String, String)>,
7197    ) -> Result<&InstanceEventWindow, Ec2Error> {
7198        let win = self
7199            .instance_event_windows
7200            .get_mut(id)
7201            .ok_or_else(|| Ec2Error::InvalidInstanceEventWindowNotFound(id.to_string()))?;
7202        let mut assoc = win.association_target.clone().unwrap_or_default();
7203        assoc.instance_ids.extend(instance_ids);
7204        assoc.dedicated_host_ids.extend(dedicated_host_ids);
7205        assoc.tags.extend(tags);
7206        win.association_target = Some(assoc);
7207        Ok(self.instance_event_windows.get(id).unwrap())
7208    }
7209
7210    pub fn disassociate_instance_event_window(
7211        &mut self,
7212        id: &str,
7213        instance_ids: Vec<String>,
7214        dedicated_host_ids: Vec<String>,
7215    ) -> Result<&InstanceEventWindow, Ec2Error> {
7216        let win = self
7217            .instance_event_windows
7218            .get_mut(id)
7219            .ok_or_else(|| Ec2Error::InvalidInstanceEventWindowNotFound(id.to_string()))?;
7220        if let Some(assoc) = win.association_target.as_mut() {
7221            assoc.instance_ids.retain(|i| !instance_ids.contains(i));
7222            assoc
7223                .dedicated_host_ids
7224                .retain(|h| !dedicated_host_ids.contains(h));
7225        }
7226        Ok(self.instance_event_windows.get(id).unwrap())
7227    }
7228
7229    pub fn modify_instance_event_start_time(
7230        &mut self,
7231        instance_id: &str,
7232        instance_event_id: &str,
7233        not_before: &str,
7234    ) -> Result<&InstanceEvent, Ec2Error> {
7235        // Synthesize the event on first lookup so callers can model upcoming
7236        // retirement windows without seeding state externally.
7237        if !self.instance_events.contains_key(instance_event_id) {
7238            if !self.instances.contains_key(instance_id) {
7239                return Err(Ec2Error::InstanceNotFound(instance_id.to_string()));
7240            }
7241            let event = InstanceEvent {
7242                event_id: instance_event_id.to_string(),
7243                instance_id: instance_id.to_string(),
7244                code: "instance-retirement".to_string(),
7245                description: "Scheduled instance retirement".to_string(),
7246                not_before: not_before.to_string(),
7247                not_after: not_before.to_string(),
7248                not_before_deadline: None,
7249            };
7250            self.instance_events
7251                .insert(instance_event_id.to_string(), event);
7252        }
7253        let event = self.instance_events.get_mut(instance_event_id).unwrap();
7254        event.not_before = not_before.to_string();
7255        Ok(self.instance_events.get(instance_event_id).unwrap())
7256    }
7257
7258    pub fn register_instance_event_notification_attributes(
7259        &mut self,
7260        include_all_tags_of_instance: Option<bool>,
7261        instance_tag_keys: Vec<String>,
7262    ) -> &InstanceTagNotificationAttributes {
7263        let mut attrs = self
7264            .instance_event_notification_attributes
7265            .clone()
7266            .unwrap_or_default();
7267        if let Some(v) = include_all_tags_of_instance {
7268            attrs.include_all_tags_of_instance = v;
7269        }
7270        for k in instance_tag_keys {
7271            if !attrs.instance_tag_keys.contains(&k) {
7272                attrs.instance_tag_keys.push(k);
7273            }
7274        }
7275        self.instance_event_notification_attributes = Some(attrs);
7276        self.instance_event_notification_attributes
7277            .as_ref()
7278            .unwrap()
7279    }
7280
7281    pub fn deregister_instance_event_notification_attributes(
7282        &mut self,
7283        include_all_tags_of_instance: Option<bool>,
7284        instance_tag_keys: &[String],
7285    ) -> InstanceTagNotificationAttributes {
7286        let mut attrs = self
7287            .instance_event_notification_attributes
7288            .clone()
7289            .unwrap_or_default();
7290        if include_all_tags_of_instance == Some(true) {
7291            attrs.include_all_tags_of_instance = false;
7292        }
7293        attrs
7294            .instance_tag_keys
7295            .retain(|k| !instance_tag_keys.contains(k));
7296        self.instance_event_notification_attributes = Some(attrs.clone());
7297        attrs
7298    }
7299
7300    // ----- Misc family -----
7301
7302    pub fn confirm_product_instance(
7303        &self,
7304        instance_id: &str,
7305        product_code: &str,
7306    ) -> Result<(String, bool), Ec2Error> {
7307        let inst = self
7308            .instances
7309            .get(instance_id)
7310            .ok_or_else(|| Ec2Error::InstanceNotFound(instance_id.to_string()))?;
7311        let matched = inst.product_codes.iter().any(|(c, _)| c == product_code);
7312        Ok((
7313            inst.owner_id.clone(),
7314            matched || inst.product_codes.is_empty(),
7315        ))
7316    }
7317
7318    pub fn delete_launch_template_versions(
7319        &mut self,
7320        lt_id: &str,
7321        versions: &[i64],
7322    ) -> Result<(Vec<i64>, Vec<i64>), Ec2Error> {
7323        let lt_id_resolved = if self.launch_templates.contains_key(lt_id) {
7324            lt_id.to_string()
7325        } else {
7326            self.launch_templates
7327                .values()
7328                .find(|lt| lt.launch_template_name == lt_id)
7329                .map(|lt| lt.launch_template_id.clone())
7330                .ok_or_else(|| Ec2Error::LaunchTemplateNotFound(lt_id.to_string()))?
7331        };
7332        let entries = self
7333            .launch_template_versions
7334            .get_mut(&lt_id_resolved)
7335            .ok_or_else(|| Ec2Error::LaunchTemplateNotFound(lt_id.to_string()))?;
7336        let mut succ = Vec::new();
7337        let mut fail = Vec::new();
7338        for v in versions {
7339            let before = entries.len();
7340            entries.retain(|e| e.version_number != *v);
7341            if entries.len() < before {
7342                succ.push(*v);
7343            } else {
7344                fail.push(*v);
7345            }
7346        }
7347        Ok((succ, fail))
7348    }
7349
7350    pub fn modify_availability_zone_group(&mut self, group_name: &str, opt_in_status: &str) {
7351        self.az_group_opt_in
7352            .insert(group_name.to_string(), opt_in_status.to_string());
7353    }
7354
7355    pub fn purchase_host_reservation(
7356        &mut self,
7357        host_id_set: Vec<String>,
7358        offering_id: &str,
7359        currency_code: Option<String>,
7360        instance_family: &str,
7361        tags: Tags,
7362    ) -> &HostReservation {
7363        let id = self.next_host_reservation_id();
7364        let now = self.now_iso();
7365        let res = HostReservation {
7366            host_reservation_id: id.clone(),
7367            host_id_set,
7368            currency_code: currency_code.unwrap_or_else(|| "USD".to_string()),
7369            duration: 31536000,
7370            end: None,
7371            hourly_price: "0.0".to_string(),
7372            instance_family: instance_family.to_string(),
7373            offering_id: offering_id.to_string(),
7374            payment_option: "NoUpfront".to_string(),
7375            start: now,
7376            state: "active".to_string(),
7377            upfront_price: "0.0".to_string(),
7378            tags,
7379        };
7380        self.host_reservations.insert(id.clone(), res);
7381        self.host_reservations.get(&id).unwrap()
7382    }
7383
7384    pub fn purchase_scheduled_instances(
7385        &mut self,
7386        requests: Vec<ScheduledInstancePurchaseRequest>,
7387    ) -> Vec<ScheduledInstance> {
7388        let mut out = Vec::new();
7389        for req in requests {
7390            let id = self.next_scheduled_instance_id();
7391            let now = self.now_iso();
7392            let si = ScheduledInstance {
7393                scheduled_instance_id: id.clone(),
7394                instance_type: req.instance_type,
7395                platform: req.platform.unwrap_or_else(|| "Linux/UNIX".to_string()),
7396                network_platform: req
7397                    .network_platform
7398                    .unwrap_or_else(|| "EC2-VPC".to_string()),
7399                availability_zone: req.availability_zone,
7400                instance_count: req.instance_count,
7401                hourly_price: req.hourly_price.unwrap_or_else(|| "0.0".to_string()),
7402                total_scheduled_instance_hours: req.total_scheduled_instance_hours,
7403                term_start_date: now.clone(),
7404                term_end_date: now.clone(),
7405                recurrence: req.recurrence,
7406                slot_duration_in_hours: req.slot_duration_in_hours,
7407                previous_slot_end_time: None,
7408                next_slot_start_time: Some(now.clone()),
7409                create_date: now,
7410            };
7411            self.scheduled_instances.insert(id, si.clone());
7412            out.push(si);
7413        }
7414        out
7415    }
7416
7417    pub fn report_instance_status(&mut self, report: InstanceStatusReport) {
7418        self.instance_status_reports.push(report);
7419    }
7420
7421    pub fn restore_managed_prefix_list_version(
7422        &mut self,
7423        prefix_list_id: &str,
7424        previous_version: i64,
7425    ) -> Result<&ManagedPrefixList, Ec2Error> {
7426        let pl = self
7427            .managed_prefix_lists
7428            .get_mut(prefix_list_id)
7429            .ok_or_else(|| Ec2Error::PrefixListNotFound(prefix_list_id.to_string()))?;
7430        let snap = pl
7431            .version_history
7432            .iter()
7433            .find(|v| v.version == previous_version)
7434            .cloned()
7435            .ok_or_else(|| {
7436                Ec2Error::InvalidParameterValue(format!(
7437                    "PreviousVersion {previous_version} does not exist"
7438                ))
7439            })?;
7440        pl.entries = snap.entries;
7441        pl.version += 1;
7442        pl.version_history.push(ManagedPrefixListVersion {
7443            version: pl.version,
7444            entries: pl.entries.clone(),
7445        });
7446        Ok(self.managed_prefix_lists.get(prefix_list_id).unwrap())
7447    }
7448
7449    pub fn run_scheduled_instances(
7450        &mut self,
7451        scheduled_instance_id: &str,
7452        instance_count: i32,
7453        owner_id: &str,
7454    ) -> Result<Vec<String>, Ec2Error> {
7455        if !self.scheduled_instances.contains_key(scheduled_instance_id) {
7456            return Err(Ec2Error::InvalidScheduledInstanceNotFound(
7457                scheduled_instance_id.to_string(),
7458            ));
7459        }
7460        let now = self.now_iso();
7461        let mut ids = Vec::new();
7462        for _ in 0..instance_count.max(1) {
7463            let instance_id = self.next_instance_id_pub();
7464            let inst = Instance {
7465                instance_id: instance_id.clone(),
7466                image_id: "ami-scheduled".to_string(),
7467                instance_type: "t3.micro".to_string(),
7468                state: InstanceState {
7469                    code: 16,
7470                    name: "running".to_string(),
7471                },
7472                private_ip_address: None,
7473                public_ip_address: None,
7474                subnet_id: None,
7475                vpc_id: None,
7476                key_name: None,
7477                security_groups: Vec::new(),
7478                launch_time: now.clone(),
7479                tags: Tags::new(),
7480                iam_instance_profile_arn: None,
7481                monitoring_state: "disabled".to_string(),
7482                placement_az: "us-east-1a".to_string(),
7483                placement_group_name: None,
7484                placement_tenancy: None,
7485                placement_host_id: None,
7486                placement_affinity: None,
7487                placement_partition_number: None,
7488                owner_id: owner_id.to_string(),
7489                classic_link_vpc: None,
7490                private_dns_hostname_type: None,
7491                enable_resource_name_dns_a_record: None,
7492                enable_resource_name_dns_aaaa_record: None,
7493                credit_specification: None,
7494                cpu_options: None,
7495                maintenance_options: None,
7496                network_bandwidth_weighting: None,
7497                lifecycle: Some("scheduled".to_string()),
7498                product_codes: Vec::new(),
7499                capacity_reservation_specification: None,
7500            };
7501            self.instances.insert(instance_id.clone(), inst);
7502            ids.push(instance_id);
7503        }
7504        Ok(ids)
7505    }
7506
7507    pub fn deregister_image_to_recycle_bin(&mut self, image_id: &str) -> Result<(), Ec2Error> {
7508        let img = self
7509            .images
7510            .get_mut(image_id)
7511            .ok_or_else(|| Ec2Error::AmiNotFound(image_id.to_string()))?;
7512        img.recycle_bin_state = Some("recycled".to_string());
7513        img.state = "deregistered".to_string();
7514        self.deregistered_images.insert(image_id.to_string());
7515        Ok(())
7516    }
7517
7518    // ===== Group 6 helpers and operations =====
7519
7520    fn next_network_insights_access_scope_id(&mut self) -> String {
7521        self.counters.network_insights_access_scope += 1;
7522        format!("nis-{:08x}", self.counters.network_insights_access_scope)
7523    }
7524
7525    fn next_network_insights_access_scope_analysis_id(&mut self) -> String {
7526        self.counters.network_insights_access_scope_analysis += 1;
7527        format!(
7528            "nisa-{:08x}",
7529            self.counters.network_insights_access_scope_analysis
7530        )
7531    }
7532
7533    fn next_network_insights_path_id(&mut self) -> String {
7534        self.counters.network_insights_path += 1;
7535        format!("nip-{:08x}", self.counters.network_insights_path)
7536    }
7537
7538    fn next_network_insights_analysis_id(&mut self) -> String {
7539        self.counters.network_insights_analysis += 1;
7540        format!("nia-{:08x}", self.counters.network_insights_analysis)
7541    }
7542
7543    fn next_traffic_mirror_filter_id(&mut self) -> String {
7544        self.counters.traffic_mirror_filter += 1;
7545        format!("tmf-{:08x}", self.counters.traffic_mirror_filter)
7546    }
7547
7548    fn next_traffic_mirror_filter_rule_id(&mut self) -> String {
7549        self.counters.traffic_mirror_filter_rule += 1;
7550        format!("tmfr-{:08x}", self.counters.traffic_mirror_filter_rule)
7551    }
7552
7553    fn next_traffic_mirror_session_id(&mut self) -> String {
7554        self.counters.traffic_mirror_session += 1;
7555        format!("tms-{:08x}", self.counters.traffic_mirror_session)
7556    }
7557
7558    fn next_traffic_mirror_target_id(&mut self) -> String {
7559        self.counters.traffic_mirror_target += 1;
7560        format!("tmt-{:08x}", self.counters.traffic_mirror_target)
7561    }
7562
7563    // ----- Network Insights access scopes -----
7564
7565    pub fn create_network_insights_access_scope(
7566        &mut self,
7567        match_paths: Vec<AccessScopePathSpec>,
7568        exclude_paths: Vec<AccessScopePathSpec>,
7569        tags: Tags,
7570    ) -> &NetworkInsightsAccessScope {
7571        let id = self.next_network_insights_access_scope_id();
7572        let now = self.now_iso();
7573        let arn = format!("arn:aws:ec2:us-east-1:123456789012:network-insights-access-scope/{id}");
7574        let scope = NetworkInsightsAccessScope {
7575            network_insights_access_scope_id: id.clone(),
7576            network_insights_access_scope_arn: arn,
7577            created_date: now.clone(),
7578            updated_date: now,
7579            tags,
7580            match_paths,
7581            exclude_paths,
7582        };
7583        self.network_insights_access_scopes
7584            .insert(id.clone(), scope);
7585        self.network_insights_access_scopes.get(&id).unwrap()
7586    }
7587
7588    pub fn delete_network_insights_access_scope(&mut self, scope_id: &str) -> Result<(), Ec2Error> {
7589        if self
7590            .network_insights_access_scopes
7591            .remove(scope_id)
7592            .is_none()
7593        {
7594            return Err(Ec2Error::InvalidNetworkInsightsAccessScopeNotFound(
7595                scope_id.to_string(),
7596            ));
7597        }
7598        Ok(())
7599    }
7600
7601    pub fn delete_network_insights_access_scope_analysis(
7602        &mut self,
7603        analysis_id: &str,
7604    ) -> Result<(), Ec2Error> {
7605        if self
7606            .network_insights_access_scope_analyses
7607            .remove(analysis_id)
7608            .is_none()
7609        {
7610            return Err(Ec2Error::InvalidNetworkInsightsAccessScopeAnalysisNotFound(
7611                analysis_id.to_string(),
7612            ));
7613        }
7614        Ok(())
7615    }
7616
7617    pub fn start_network_insights_access_scope_analysis(
7618        &mut self,
7619        scope_id: &str,
7620        tags: Tags,
7621    ) -> Result<&NetworkInsightsAccessScopeAnalysis, Ec2Error> {
7622        if !self.network_insights_access_scopes.contains_key(scope_id) {
7623            return Err(Ec2Error::InvalidNetworkInsightsAccessScopeNotFound(
7624                scope_id.to_string(),
7625            ));
7626        }
7627        let id = self.next_network_insights_access_scope_analysis_id();
7628        let now = self.now_iso();
7629        let arn = format!(
7630            "arn:aws:ec2:us-east-1:123456789012:network-insights-access-scope-analysis/{id}"
7631        );
7632        let analysis = NetworkInsightsAccessScopeAnalysis {
7633            network_insights_access_scope_analysis_id: id.clone(),
7634            network_insights_access_scope_analysis_arn: arn,
7635            network_insights_access_scope_id: scope_id.to_string(),
7636            status: "succeeded".to_string(),
7637            status_message: None,
7638            warning_message: None,
7639            start_date: now.clone(),
7640            end_date: Some(now),
7641            findings_found: "false".to_string(),
7642            analyzed_eni_count: 0,
7643            tags,
7644        };
7645        self.network_insights_access_scope_analyses
7646            .insert(id.clone(), analysis);
7647        Ok(self
7648            .network_insights_access_scope_analyses
7649            .get(&id)
7650            .unwrap())
7651    }
7652
7653    // ----- Network Insights paths -----
7654
7655    #[allow(clippy::too_many_arguments)]
7656    pub fn create_network_insights_path(
7657        &mut self,
7658        source: Option<String>,
7659        destination: Option<String>,
7660        source_arn: Option<String>,
7661        destination_arn: Option<String>,
7662        source_ip: Option<String>,
7663        destination_ip: Option<String>,
7664        protocol: String,
7665        destination_port: Option<i32>,
7666        filter_at_source: NetworkInsightsPathFilter,
7667        filter_at_destination: NetworkInsightsPathFilter,
7668        tags: Tags,
7669    ) -> &NetworkInsightsPath {
7670        let id = self.next_network_insights_path_id();
7671        let now = self.now_iso();
7672        let arn = format!("arn:aws:ec2:us-east-1:123456789012:network-insights-path/{id}");
7673        let path = NetworkInsightsPath {
7674            network_insights_path_id: id.clone(),
7675            network_insights_path_arn: arn,
7676            created_date: now,
7677            source,
7678            destination,
7679            source_arn,
7680            destination_arn,
7681            source_ip,
7682            destination_ip,
7683            protocol,
7684            destination_port,
7685            tags,
7686            filter_at_source,
7687            filter_at_destination,
7688        };
7689        self.network_insights_paths.insert(id.clone(), path);
7690        self.network_insights_paths.get(&id).unwrap()
7691    }
7692
7693    pub fn delete_network_insights_path(&mut self, path_id: &str) -> Result<(), Ec2Error> {
7694        if !self.network_insights_paths.contains_key(path_id) {
7695            return Err(Ec2Error::InvalidNetworkInsightsPathNotFound(
7696                path_id.to_string(),
7697            ));
7698        }
7699        let has_analyses = self
7700            .network_insights_analyses
7701            .values()
7702            .any(|a| a.network_insights_path_id == path_id);
7703        if has_analyses {
7704            return Err(Ec2Error::NetworkInsightsPathHasAnalyses(
7705                path_id.to_string(),
7706            ));
7707        }
7708        self.network_insights_paths.remove(path_id);
7709        Ok(())
7710    }
7711
7712    pub fn delete_network_insights_analysis(&mut self, analysis_id: &str) -> Result<(), Ec2Error> {
7713        if self.network_insights_analyses.remove(analysis_id).is_none() {
7714            return Err(Ec2Error::InvalidNetworkInsightsAnalysisNotFound(
7715                analysis_id.to_string(),
7716            ));
7717        }
7718        Ok(())
7719    }
7720
7721    pub fn start_network_insights_analysis(
7722        &mut self,
7723        path_id: &str,
7724        additional_accounts: Vec<String>,
7725        filter_in_arns: Vec<String>,
7726        tags: Tags,
7727    ) -> Result<&NetworkInsightsAnalysis, Ec2Error> {
7728        if !self.network_insights_paths.contains_key(path_id) {
7729            return Err(Ec2Error::InvalidNetworkInsightsPathNotFound(
7730                path_id.to_string(),
7731            ));
7732        }
7733        let id = self.next_network_insights_analysis_id();
7734        let now = self.now_iso();
7735        let arn = format!("arn:aws:ec2:us-east-1:123456789012:network-insights-analysis/{id}");
7736        let analysis = NetworkInsightsAnalysis {
7737            network_insights_analysis_id: id.clone(),
7738            network_insights_analysis_arn: arn,
7739            network_insights_path_id: path_id.to_string(),
7740            additional_accounts,
7741            filter_in_arns,
7742            start_date: now.clone(),
7743            end_date: Some(now),
7744            status: "succeeded".to_string(),
7745            status_message: None,
7746            warning_message: None,
7747            network_path_found: true,
7748            tags,
7749        };
7750        self.network_insights_analyses.insert(id.clone(), analysis);
7751        Ok(self.network_insights_analyses.get(&id).unwrap())
7752    }
7753
7754    // ----- Traffic Mirror filters -----
7755
7756    pub fn create_traffic_mirror_filter(
7757        &mut self,
7758        description: Option<String>,
7759        tags: Tags,
7760    ) -> &TrafficMirrorFilter {
7761        let id = self.next_traffic_mirror_filter_id();
7762        let filter = TrafficMirrorFilter {
7763            traffic_mirror_filter_id: id.clone(),
7764            description,
7765            ingress_filter_rules: Vec::new(),
7766            egress_filter_rules: Vec::new(),
7767            network_services: Vec::new(),
7768            tags,
7769        };
7770        self.traffic_mirror_filters.insert(id.clone(), filter);
7771        self.traffic_mirror_filters.get(&id).unwrap()
7772    }
7773
7774    pub fn delete_traffic_mirror_filter(&mut self, filter_id: &str) -> Result<(), Ec2Error> {
7775        if !self.traffic_mirror_filters.contains_key(filter_id) {
7776            return Err(Ec2Error::InvalidTrafficMirrorFilterNotFound(
7777                filter_id.to_string(),
7778            ));
7779        }
7780        if self
7781            .traffic_mirror_sessions
7782            .values()
7783            .any(|s| s.traffic_mirror_filter_id == filter_id)
7784        {
7785            return Err(Ec2Error::TrafficMirrorFilterInUse(filter_id.to_string()));
7786        }
7787        self.traffic_mirror_filters.remove(filter_id);
7788        Ok(())
7789    }
7790
7791    #[allow(clippy::too_many_arguments)]
7792    pub fn create_traffic_mirror_filter_rule(
7793        &mut self,
7794        filter_id: &str,
7795        traffic_direction: String,
7796        rule_number: i32,
7797        rule_action: String,
7798        protocol: Option<i32>,
7799        destination_port_range: Option<TrafficMirrorPortRange>,
7800        source_port_range: Option<TrafficMirrorPortRange>,
7801        destination_cidr_block: String,
7802        source_cidr_block: String,
7803        description: Option<String>,
7804        tags: Tags,
7805    ) -> Result<TrafficMirrorFilterRule, Ec2Error> {
7806        if !self.traffic_mirror_filters.contains_key(filter_id) {
7807            return Err(Ec2Error::InvalidTrafficMirrorFilterNotFound(
7808                filter_id.to_string(),
7809            ));
7810        }
7811        let rule_id = self.next_traffic_mirror_filter_rule_id();
7812        let rule = TrafficMirrorFilterRule {
7813            traffic_mirror_filter_rule_id: rule_id,
7814            traffic_mirror_filter_id: filter_id.to_string(),
7815            traffic_direction: traffic_direction.clone(),
7816            rule_number,
7817            rule_action,
7818            protocol,
7819            destination_port_range,
7820            source_port_range,
7821            destination_cidr_block,
7822            source_cidr_block,
7823            description,
7824            tags,
7825        };
7826        let filter = self.traffic_mirror_filters.get_mut(filter_id).unwrap();
7827        if traffic_direction == "egress" {
7828            filter.egress_filter_rules.push(rule.clone());
7829        } else {
7830            filter.ingress_filter_rules.push(rule.clone());
7831        }
7832        Ok(rule)
7833    }
7834
7835    pub fn delete_traffic_mirror_filter_rule(&mut self, rule_id: &str) -> Result<(), Ec2Error> {
7836        for filter in self.traffic_mirror_filters.values_mut() {
7837            let before_in = filter.ingress_filter_rules.len();
7838            filter
7839                .ingress_filter_rules
7840                .retain(|r| r.traffic_mirror_filter_rule_id != rule_id);
7841            let before_out = filter.egress_filter_rules.len();
7842            filter
7843                .egress_filter_rules
7844                .retain(|r| r.traffic_mirror_filter_rule_id != rule_id);
7845            if filter.ingress_filter_rules.len() != before_in
7846                || filter.egress_filter_rules.len() != before_out
7847            {
7848                return Ok(());
7849            }
7850        }
7851        Err(Ec2Error::InvalidTrafficMirrorFilterRuleNotFound(
7852            rule_id.to_string(),
7853        ))
7854    }
7855
7856    #[allow(clippy::too_many_arguments)]
7857    pub fn modify_traffic_mirror_filter_rule(
7858        &mut self,
7859        rule_id: &str,
7860        rule_number: Option<i32>,
7861        rule_action: Option<String>,
7862        protocol: Option<i32>,
7863        destination_port_range: Option<TrafficMirrorPortRange>,
7864        source_port_range: Option<TrafficMirrorPortRange>,
7865        destination_cidr_block: Option<String>,
7866        source_cidr_block: Option<String>,
7867        description: Option<String>,
7868        traffic_direction: Option<String>,
7869        remove_fields: &[String],
7870    ) -> Result<TrafficMirrorFilterRule, Ec2Error> {
7871        for filter in self.traffic_mirror_filters.values_mut() {
7872            for rule in filter
7873                .ingress_filter_rules
7874                .iter_mut()
7875                .chain(filter.egress_filter_rules.iter_mut())
7876            {
7877                if rule.traffic_mirror_filter_rule_id == rule_id {
7878                    if let Some(n) = rule_number {
7879                        rule.rule_number = n;
7880                    }
7881                    if let Some(a) = rule_action {
7882                        rule.rule_action = a;
7883                    }
7884                    if protocol.is_some() {
7885                        rule.protocol = protocol;
7886                    }
7887                    if destination_port_range.is_some() {
7888                        rule.destination_port_range = destination_port_range;
7889                    }
7890                    if source_port_range.is_some() {
7891                        rule.source_port_range = source_port_range;
7892                    }
7893                    if let Some(c) = destination_cidr_block {
7894                        rule.destination_cidr_block = c;
7895                    }
7896                    if let Some(c) = source_cidr_block {
7897                        rule.source_cidr_block = c;
7898                    }
7899                    if description.is_some() {
7900                        rule.description = description;
7901                    }
7902                    if let Some(td) = traffic_direction {
7903                        rule.traffic_direction = td;
7904                    }
7905                    for f in remove_fields {
7906                        match f.as_str() {
7907                            "protocol" => rule.protocol = None,
7908                            "description" => rule.description = None,
7909                            "destination-port-range" => rule.destination_port_range = None,
7910                            "source-port-range" => rule.source_port_range = None,
7911                            _ => {}
7912                        }
7913                    }
7914                    return Ok(rule.clone());
7915                }
7916            }
7917        }
7918        Err(Ec2Error::InvalidTrafficMirrorFilterRuleNotFound(
7919            rule_id.to_string(),
7920        ))
7921    }
7922
7923    pub fn modify_traffic_mirror_filter_network_services(
7924        &mut self,
7925        filter_id: &str,
7926        add: &[String],
7927        remove: &[String],
7928    ) -> Result<TrafficMirrorFilter, Ec2Error> {
7929        let filter = self
7930            .traffic_mirror_filters
7931            .get_mut(filter_id)
7932            .ok_or_else(|| Ec2Error::InvalidTrafficMirrorFilterNotFound(filter_id.to_string()))?;
7933        for s in add {
7934            if !filter.network_services.contains(s) {
7935                filter.network_services.push(s.clone());
7936            }
7937        }
7938        filter.network_services.retain(|s| !remove.contains(s));
7939        Ok(filter.clone())
7940    }
7941
7942    // ----- Traffic Mirror sessions -----
7943
7944    #[allow(clippy::too_many_arguments)]
7945    pub fn create_traffic_mirror_session(
7946        &mut self,
7947        traffic_mirror_target_id: String,
7948        traffic_mirror_filter_id: String,
7949        network_interface_id: String,
7950        owner_id: String,
7951        packet_length: Option<i32>,
7952        session_number: i32,
7953        virtual_network_id: Option<i32>,
7954        description: Option<String>,
7955        tags: Tags,
7956    ) -> Result<&TrafficMirrorSession, Ec2Error> {
7957        if !self
7958            .traffic_mirror_filters
7959            .contains_key(&traffic_mirror_filter_id)
7960        {
7961            return Err(Ec2Error::InvalidTrafficMirrorFilterNotFound(
7962                traffic_mirror_filter_id,
7963            ));
7964        }
7965        if !self
7966            .traffic_mirror_targets
7967            .contains_key(&traffic_mirror_target_id)
7968        {
7969            return Err(Ec2Error::InvalidTrafficMirrorTargetNotFound(
7970                traffic_mirror_target_id,
7971            ));
7972        }
7973        let id = self.next_traffic_mirror_session_id();
7974        let session = TrafficMirrorSession {
7975            traffic_mirror_session_id: id.clone(),
7976            traffic_mirror_target_id,
7977            traffic_mirror_filter_id,
7978            network_interface_id,
7979            owner_id,
7980            packet_length,
7981            session_number,
7982            virtual_network_id,
7983            description,
7984            tags,
7985        };
7986        self.traffic_mirror_sessions.insert(id.clone(), session);
7987        Ok(self.traffic_mirror_sessions.get(&id).unwrap())
7988    }
7989
7990    pub fn delete_traffic_mirror_session(&mut self, session_id: &str) -> Result<(), Ec2Error> {
7991        if self.traffic_mirror_sessions.remove(session_id).is_none() {
7992            return Err(Ec2Error::InvalidTrafficMirrorSessionNotFound(
7993                session_id.to_string(),
7994            ));
7995        }
7996        Ok(())
7997    }
7998
7999    #[allow(clippy::too_many_arguments)]
8000    pub fn modify_traffic_mirror_session(
8001        &mut self,
8002        session_id: &str,
8003        traffic_mirror_target_id: Option<String>,
8004        traffic_mirror_filter_id: Option<String>,
8005        packet_length: Option<i32>,
8006        session_number: Option<i32>,
8007        virtual_network_id: Option<i32>,
8008        description: Option<String>,
8009        remove_fields: &[String],
8010    ) -> Result<TrafficMirrorSession, Ec2Error> {
8011        let session = self
8012            .traffic_mirror_sessions
8013            .get_mut(session_id)
8014            .ok_or_else(|| Ec2Error::InvalidTrafficMirrorSessionNotFound(session_id.to_string()))?;
8015        if let Some(t) = traffic_mirror_target_id {
8016            session.traffic_mirror_target_id = t;
8017        }
8018        if let Some(f) = traffic_mirror_filter_id {
8019            session.traffic_mirror_filter_id = f;
8020        }
8021        if packet_length.is_some() {
8022            session.packet_length = packet_length;
8023        }
8024        if let Some(n) = session_number {
8025            session.session_number = n;
8026        }
8027        if virtual_network_id.is_some() {
8028            session.virtual_network_id = virtual_network_id;
8029        }
8030        if description.is_some() {
8031            session.description = description;
8032        }
8033        for f in remove_fields {
8034            match f.as_str() {
8035                "packet-length" => session.packet_length = None,
8036                "description" => session.description = None,
8037                "virtual-network-id" => session.virtual_network_id = None,
8038                _ => {}
8039            }
8040        }
8041        Ok(session.clone())
8042    }
8043
8044    // ----- Traffic Mirror targets -----
8045
8046    #[allow(clippy::too_many_arguments)]
8047    pub fn create_traffic_mirror_target(
8048        &mut self,
8049        network_interface_id: Option<String>,
8050        network_load_balancer_arn: Option<String>,
8051        gateway_load_balancer_endpoint_id: Option<String>,
8052        description: Option<String>,
8053        owner_id: String,
8054        tags: Tags,
8055    ) -> &TrafficMirrorTarget {
8056        let id = self.next_traffic_mirror_target_id();
8057        let r#type = if network_load_balancer_arn.is_some() {
8058            "network-load-balancer".to_string()
8059        } else if gateway_load_balancer_endpoint_id.is_some() {
8060            "gateway-load-balancer-endpoint".to_string()
8061        } else {
8062            "network-interface".to_string()
8063        };
8064        let target = TrafficMirrorTarget {
8065            traffic_mirror_target_id: id.clone(),
8066            network_interface_id,
8067            network_load_balancer_arn,
8068            gateway_load_balancer_endpoint_id,
8069            r#type,
8070            description,
8071            owner_id,
8072            tags,
8073        };
8074        self.traffic_mirror_targets.insert(id.clone(), target);
8075        self.traffic_mirror_targets.get(&id).unwrap()
8076    }
8077
8078    pub fn delete_traffic_mirror_target(&mut self, target_id: &str) -> Result<(), Ec2Error> {
8079        if !self.traffic_mirror_targets.contains_key(target_id) {
8080            return Err(Ec2Error::InvalidTrafficMirrorTargetNotFound(
8081                target_id.to_string(),
8082            ));
8083        }
8084        if self
8085            .traffic_mirror_sessions
8086            .values()
8087            .any(|s| s.traffic_mirror_target_id == target_id)
8088        {
8089            return Err(Ec2Error::TrafficMirrorTargetInUse(target_id.to_string()));
8090        }
8091        self.traffic_mirror_targets.remove(target_id);
8092        Ok(())
8093    }
8094
8095    // ===== Group 7: Client VPN =====
8096
8097    fn next_client_vpn_endpoint_id(&mut self) -> String {
8098        self.counters.client_vpn_endpoint += 1;
8099        format!("cvpn-endpoint-{:08x}", self.counters.client_vpn_endpoint)
8100    }
8101
8102    fn next_client_vpn_target_network_association_id(&mut self) -> String {
8103        self.counters.client_vpn_target_network_association += 1;
8104        format!(
8105            "cvpn-assoc-{:08x}",
8106            self.counters.client_vpn_target_network_association
8107        )
8108    }
8109
8110    fn next_client_vpn_connection_id(&mut self) -> String {
8111        self.counters.client_vpn_connection += 1;
8112        format!(
8113            "cvpn-connection-{:08x}",
8114            self.counters.client_vpn_connection
8115        )
8116    }
8117
8118    #[allow(clippy::too_many_arguments)]
8119    pub fn create_client_vpn_endpoint(
8120        &mut self,
8121        description: Option<String>,
8122        client_cidr_block: String,
8123        server_certificate_arn: String,
8124        dns_servers: Vec<String>,
8125        transport_protocol: String,
8126        vpn_port: i32,
8127        split_tunnel: bool,
8128        authentication_options: Vec<String>,
8129        connection_log_options_enabled: bool,
8130        connection_log_options_cloudwatch_log_group: Option<String>,
8131        connection_log_options_cloudwatch_log_stream: Option<String>,
8132        security_group_ids: Vec<String>,
8133        vpc_id: Option<String>,
8134        self_service_portal: String,
8135        session_timeout_hours: i32,
8136        client_login_banner_enabled: bool,
8137        client_login_banner_text: Option<String>,
8138        disconnect_on_session_timeout: bool,
8139        client_route_enforcement_enforced: bool,
8140        tags: Tags,
8141    ) -> &ClientVpnEndpoint {
8142        let id = self.next_client_vpn_endpoint_id();
8143        let dns_name = format!("{id}.cvpn-endpoint.amazonaws.com");
8144        let endpoint = ClientVpnEndpoint {
8145            client_vpn_endpoint_id: id.clone(),
8146            description,
8147            status: ClientVpnEndpointStatus {
8148                code: "pending-associate".to_string(),
8149                message: None,
8150            },
8151            creation_time: self.now_iso(),
8152            deletion_time: None,
8153            dns_name,
8154            client_cidr_block,
8155            dns_servers,
8156            split_tunnel,
8157            vpn_protocol: "openvpn".to_string(),
8158            transport_protocol,
8159            vpn_port,
8160            server_certificate_arn,
8161            authentication_options,
8162            connection_log_options_enabled,
8163            connection_log_options_cloudwatch_log_group,
8164            connection_log_options_cloudwatch_log_stream,
8165            tags,
8166            security_group_ids,
8167            vpc_id,
8168            self_service_portal_url: None,
8169            self_service_portal,
8170            session_timeout_hours,
8171            client_login_banner_enabled,
8172            client_login_banner_text,
8173            disconnect_on_session_timeout,
8174            client_route_enforcement_enforced,
8175            client_certificate_revocation_list: None,
8176        };
8177        self.client_vpn_endpoints.insert(id.clone(), endpoint);
8178        self.client_vpn_endpoints.get(&id).unwrap()
8179    }
8180
8181    pub fn delete_client_vpn_endpoint(
8182        &mut self,
8183        endpoint_id: &str,
8184    ) -> Result<ClientVpnEndpointStatus, Ec2Error> {
8185        if !self.client_vpn_endpoints.contains_key(endpoint_id) {
8186            return Err(Ec2Error::InvalidClientVpnEndpointNotFound(
8187                endpoint_id.to_string(),
8188            ));
8189        }
8190        let has_assoc = self
8191            .client_vpn_target_network_associations
8192            .values()
8193            .any(|a| a.client_vpn_endpoint_id == endpoint_id);
8194        let has_routes = self
8195            .client_vpn_routes
8196            .keys()
8197            .any(|(eid, _, _)| eid == endpoint_id);
8198        if has_assoc || has_routes {
8199            return Err(Ec2Error::ClientVpnEndpointInUse(endpoint_id.to_string()));
8200        }
8201        self.client_vpn_endpoints.remove(endpoint_id);
8202        Ok(ClientVpnEndpointStatus {
8203            code: "deleting".to_string(),
8204            message: None,
8205        })
8206    }
8207
8208    pub fn associate_client_vpn_target_network(
8209        &mut self,
8210        endpoint_id: &str,
8211        subnet_id: &str,
8212    ) -> Result<&ClientVpnTargetNetworkAssociation, Ec2Error> {
8213        if !self.client_vpn_endpoints.contains_key(endpoint_id) {
8214            return Err(Ec2Error::InvalidClientVpnEndpointNotFound(
8215                endpoint_id.to_string(),
8216            ));
8217        }
8218        let vpc_id = self
8219            .subnets
8220            .get(subnet_id)
8221            .map(|s| s.vpc_id.clone())
8222            .unwrap_or_default();
8223        let assoc_id = self.next_client_vpn_target_network_association_id();
8224        let assoc = ClientVpnTargetNetworkAssociation {
8225            association_id: assoc_id.clone(),
8226            vpc_id: vpc_id.clone(),
8227            target_network_id: subnet_id.to_string(),
8228            client_vpn_endpoint_id: endpoint_id.to_string(),
8229            security_groups: Vec::new(),
8230            status: ClientVpnAssociationStatus {
8231                code: "associating".to_string(),
8232                message: None,
8233            },
8234        };
8235        self.client_vpn_target_network_associations
8236            .insert(assoc_id.clone(), assoc);
8237        // Bind the endpoint to the VPC if not already.
8238        if let Some(ep) = self.client_vpn_endpoints.get_mut(endpoint_id)
8239            && ep.vpc_id.is_none()
8240            && !vpc_id.is_empty()
8241        {
8242            ep.vpc_id = Some(vpc_id);
8243        }
8244        Ok(self
8245            .client_vpn_target_network_associations
8246            .get(&assoc_id)
8247            .unwrap())
8248    }
8249
8250    pub fn disassociate_client_vpn_target_network(
8251        &mut self,
8252        endpoint_id: &str,
8253        association_id: &str,
8254    ) -> Result<ClientVpnAssociationStatus, Ec2Error> {
8255        if !self.client_vpn_endpoints.contains_key(endpoint_id) {
8256            return Err(Ec2Error::InvalidClientVpnEndpointNotFound(
8257                endpoint_id.to_string(),
8258            ));
8259        }
8260        let assoc = self
8261            .client_vpn_target_network_associations
8262            .remove(association_id)
8263            .ok_or_else(|| {
8264                Ec2Error::InvalidClientVpnTargetNetworkAssociationNotFound(
8265                    association_id.to_string(),
8266                )
8267            })?;
8268        if assoc.client_vpn_endpoint_id != endpoint_id {
8269            // Restore and refuse.
8270            self.client_vpn_target_network_associations
8271                .insert(association_id.to_string(), assoc);
8272            return Err(Ec2Error::InvalidClientVpnTargetNetworkAssociationNotFound(
8273                association_id.to_string(),
8274            ));
8275        }
8276        Ok(ClientVpnAssociationStatus {
8277            code: "disassociating".to_string(),
8278            message: None,
8279        })
8280    }
8281
8282    pub fn apply_security_groups_to_client_vpn_target_network(
8283        &mut self,
8284        endpoint_id: &str,
8285        vpc_id: &str,
8286        security_group_ids: Vec<String>,
8287    ) -> Result<Vec<String>, Ec2Error> {
8288        if !self.client_vpn_endpoints.contains_key(endpoint_id) {
8289            return Err(Ec2Error::InvalidClientVpnEndpointNotFound(
8290                endpoint_id.to_string(),
8291            ));
8292        }
8293        let mut applied = false;
8294        for assoc in self.client_vpn_target_network_associations.values_mut() {
8295            if assoc.client_vpn_endpoint_id == endpoint_id && assoc.vpc_id == vpc_id {
8296                assoc.security_groups = security_group_ids.clone();
8297                applied = true;
8298            }
8299        }
8300        // Also remember on the endpoint itself.
8301        if let Some(ep) = self.client_vpn_endpoints.get_mut(endpoint_id) {
8302            ep.security_group_ids = security_group_ids.clone();
8303        }
8304        if !applied {
8305            // No matching association is fine; AWS still records the change at endpoint scope.
8306        }
8307        Ok(security_group_ids)
8308    }
8309
8310    #[allow(clippy::too_many_arguments)]
8311    pub fn authorize_client_vpn_ingress(
8312        &mut self,
8313        endpoint_id: &str,
8314        destination_cidr: String,
8315        access_all: bool,
8316        group_id: Option<String>,
8317        description: Option<String>,
8318    ) -> Result<ClientVpnAuthorizationRuleStatus, Ec2Error> {
8319        if !self.client_vpn_endpoints.contains_key(endpoint_id) {
8320            return Err(Ec2Error::InvalidClientVpnEndpointNotFound(
8321                endpoint_id.to_string(),
8322            ));
8323        }
8324        let key = (
8325            endpoint_id.to_string(),
8326            destination_cidr.clone(),
8327            group_id.clone().unwrap_or_default(),
8328        );
8329        let status = ClientVpnAuthorizationRuleStatus {
8330            code: "active".to_string(),
8331            message: None,
8332        };
8333        let rule = ClientVpnAuthorizationRule {
8334            client_vpn_endpoint_id: endpoint_id.to_string(),
8335            group_id,
8336            access_all,
8337            destination_cidr,
8338            description,
8339            status: status.clone(),
8340        };
8341        self.client_vpn_authorization_rules.insert(key, rule);
8342        Ok(status)
8343    }
8344
8345    pub fn revoke_client_vpn_ingress(
8346        &mut self,
8347        endpoint_id: &str,
8348        destination_cidr: &str,
8349        group_id: Option<String>,
8350    ) -> Result<ClientVpnAuthorizationRuleStatus, Ec2Error> {
8351        if !self.client_vpn_endpoints.contains_key(endpoint_id) {
8352            return Err(Ec2Error::InvalidClientVpnEndpointNotFound(
8353                endpoint_id.to_string(),
8354            ));
8355        }
8356        let key = (
8357            endpoint_id.to_string(),
8358            destination_cidr.to_string(),
8359            group_id.unwrap_or_default(),
8360        );
8361        if self.client_vpn_authorization_rules.remove(&key).is_some() {
8362            Ok(ClientVpnAuthorizationRuleStatus {
8363                code: "revoking".to_string(),
8364                message: None,
8365            })
8366        } else {
8367            Err(Ec2Error::InvalidClientVpnAuthorizationRuleNotFound(
8368                endpoint_id.to_string(),
8369                destination_cidr.to_string(),
8370            ))
8371        }
8372    }
8373
8374    pub fn create_client_vpn_route(
8375        &mut self,
8376        endpoint_id: &str,
8377        destination_cidr: String,
8378        target_subnet: String,
8379        description: Option<String>,
8380    ) -> Result<ClientVpnRouteStatus, Ec2Error> {
8381        if !self.client_vpn_endpoints.contains_key(endpoint_id) {
8382            return Err(Ec2Error::InvalidClientVpnEndpointNotFound(
8383                endpoint_id.to_string(),
8384            ));
8385        }
8386        let key = (
8387            endpoint_id.to_string(),
8388            destination_cidr.clone(),
8389            target_subnet.clone(),
8390        );
8391        let status = ClientVpnRouteStatus {
8392            code: "creating".to_string(),
8393            message: None,
8394        };
8395        let route = ClientVpnRoute {
8396            client_vpn_endpoint_id: endpoint_id.to_string(),
8397            destination_cidr,
8398            target_subnet,
8399            r#type: "Nat".to_string(),
8400            origin: "add-route".to_string(),
8401            status: status.clone(),
8402            description,
8403        };
8404        self.client_vpn_routes.insert(key, route);
8405        Ok(status)
8406    }
8407
8408    pub fn delete_client_vpn_route(
8409        &mut self,
8410        endpoint_id: &str,
8411        destination_cidr: &str,
8412        target_subnet: &str,
8413    ) -> Result<ClientVpnRouteStatus, Ec2Error> {
8414        if !self.client_vpn_endpoints.contains_key(endpoint_id) {
8415            return Err(Ec2Error::InvalidClientVpnEndpointNotFound(
8416                endpoint_id.to_string(),
8417            ));
8418        }
8419        let key = (
8420            endpoint_id.to_string(),
8421            destination_cidr.to_string(),
8422            target_subnet.to_string(),
8423        );
8424        if self.client_vpn_routes.remove(&key).is_some() {
8425            Ok(ClientVpnRouteStatus {
8426                code: "deleting".to_string(),
8427                message: None,
8428            })
8429        } else {
8430            Err(Ec2Error::InvalidClientVpnRouteNotFound(
8431                endpoint_id.to_string(),
8432                destination_cidr.to_string(),
8433                target_subnet.to_string(),
8434            ))
8435        }
8436    }
8437
8438    pub fn import_client_vpn_revocation_list(
8439        &mut self,
8440        endpoint_id: &str,
8441        crl: String,
8442    ) -> Result<(), Ec2Error> {
8443        let ep = self
8444            .client_vpn_endpoints
8445            .get_mut(endpoint_id)
8446            .ok_or_else(|| Ec2Error::InvalidClientVpnEndpointNotFound(endpoint_id.to_string()))?;
8447        ep.client_certificate_revocation_list = Some(crl);
8448        Ok(())
8449    }
8450
8451    #[allow(clippy::too_many_arguments)]
8452    pub fn modify_client_vpn_endpoint(
8453        &mut self,
8454        endpoint_id: &str,
8455        server_certificate_arn: Option<String>,
8456        connection_log_options_enabled: Option<bool>,
8457        connection_log_options_cloudwatch_log_group: Option<String>,
8458        connection_log_options_cloudwatch_log_stream: Option<String>,
8459        dns_servers_custom: Option<Vec<String>>,
8460        dns_servers_enabled: Option<bool>,
8461        vpn_port: Option<i32>,
8462        description: Option<String>,
8463        split_tunnel: Option<bool>,
8464        security_group_ids: Option<Vec<String>>,
8465        vpc_id: Option<String>,
8466        self_service_portal: Option<String>,
8467        session_timeout_hours: Option<i32>,
8468        client_login_banner_enabled: Option<bool>,
8469        client_login_banner_text: Option<String>,
8470        disconnect_on_session_timeout: Option<bool>,
8471        client_route_enforcement_enforced: Option<bool>,
8472    ) -> Result<(), Ec2Error> {
8473        let ep = self
8474            .client_vpn_endpoints
8475            .get_mut(endpoint_id)
8476            .ok_or_else(|| Ec2Error::InvalidClientVpnEndpointNotFound(endpoint_id.to_string()))?;
8477        if let Some(s) = server_certificate_arn {
8478            ep.server_certificate_arn = s;
8479        }
8480        if let Some(b) = connection_log_options_enabled {
8481            ep.connection_log_options_enabled = b;
8482        }
8483        if connection_log_options_cloudwatch_log_group.is_some() {
8484            ep.connection_log_options_cloudwatch_log_group =
8485                connection_log_options_cloudwatch_log_group;
8486        }
8487        if connection_log_options_cloudwatch_log_stream.is_some() {
8488            ep.connection_log_options_cloudwatch_log_stream =
8489                connection_log_options_cloudwatch_log_stream;
8490        }
8491        if let Some(enabled) = dns_servers_enabled {
8492            if !enabled {
8493                ep.dns_servers.clear();
8494            } else if let Some(custom) = dns_servers_custom {
8495                ep.dns_servers = custom;
8496            }
8497        } else if let Some(custom) = dns_servers_custom {
8498            ep.dns_servers = custom;
8499        }
8500        if let Some(p) = vpn_port {
8501            ep.vpn_port = p;
8502        }
8503        if description.is_some() {
8504            ep.description = description;
8505        }
8506        if let Some(b) = split_tunnel {
8507            ep.split_tunnel = b;
8508        }
8509        if let Some(s) = security_group_ids {
8510            ep.security_group_ids = s;
8511        }
8512        if vpc_id.is_some() {
8513            ep.vpc_id = vpc_id;
8514        }
8515        if let Some(s) = self_service_portal {
8516            ep.self_service_portal = s;
8517        }
8518        if let Some(h) = session_timeout_hours {
8519            ep.session_timeout_hours = h;
8520        }
8521        if let Some(b) = client_login_banner_enabled {
8522            ep.client_login_banner_enabled = b;
8523        }
8524        if client_login_banner_text.is_some() {
8525            ep.client_login_banner_text = client_login_banner_text;
8526        }
8527        if let Some(b) = disconnect_on_session_timeout {
8528            ep.disconnect_on_session_timeout = b;
8529        }
8530        if let Some(b) = client_route_enforcement_enforced {
8531            ep.client_route_enforcement_enforced = b;
8532        }
8533        Ok(())
8534    }
8535
8536    /// Seeds a client VPN connection record. Used by tests.
8537    #[allow(dead_code)]
8538    pub fn seed_client_vpn_connection(
8539        &mut self,
8540        endpoint_id: &str,
8541        username: Option<String>,
8542    ) -> String {
8543        let id = self.next_client_vpn_connection_id();
8544        let now = self.now_iso();
8545        let conn = ClientVpnConnection {
8546            connection_id: id.clone(),
8547            client_vpn_endpoint_id: endpoint_id.to_string(),
8548            username,
8549            status: ClientVpnConnectionStatus {
8550                code: "active".to_string(),
8551                message: None,
8552            },
8553            posture_compliance_statuses: Vec::new(),
8554            common_name: None,
8555            connection_established_time: now.clone(),
8556            connection_end_time: None,
8557            ingress_bytes: "0".to_string(),
8558            egress_bytes: "0".to_string(),
8559            ingress_packets: "0".to_string(),
8560            egress_packets: "0".to_string(),
8561            client_ip: None,
8562            client_port: None,
8563            timestamp: now,
8564        };
8565        self.client_vpn_connections.insert(id.clone(), conn);
8566        id
8567    }
8568
8569    /// Terminate connections by id list (fallback by username if list empty).
8570    /// Returns the (current, previous) status per connection.
8571    pub fn terminate_client_vpn_connections(
8572        &mut self,
8573        endpoint_id: &str,
8574        connection_ids: Vec<String>,
8575        username: Option<String>,
8576    ) -> Result<Vec<(String, ClientVpnConnectionStatus, ClientVpnConnectionStatus)>, Ec2Error> {
8577        if !self.client_vpn_endpoints.contains_key(endpoint_id) {
8578            return Err(Ec2Error::InvalidClientVpnEndpointNotFound(
8579                endpoint_id.to_string(),
8580            ));
8581        }
8582        let mut out = Vec::new();
8583        let to_terminate: Vec<String> = if !connection_ids.is_empty() {
8584            connection_ids
8585        } else if let Some(u) = username.as_ref() {
8586            self.client_vpn_connections
8587                .values()
8588                .filter(|c| {
8589                    c.client_vpn_endpoint_id == endpoint_id
8590                        && c.username.as_deref() == Some(u.as_str())
8591                })
8592                .map(|c| c.connection_id.clone())
8593                .collect()
8594        } else {
8595            Vec::new()
8596        };
8597        for id in to_terminate {
8598            if let Some(conn) = self.client_vpn_connections.get_mut(&id) {
8599                let prev = conn.status.clone();
8600                conn.status = ClientVpnConnectionStatus {
8601                    code: "terminating".to_string(),
8602                    message: None,
8603                };
8604                out.push((id, conn.status.clone(), prev));
8605            }
8606        }
8607        Ok(out)
8608    }
8609
8610    // ===== Group 7: Local Gateway =====
8611
8612    fn next_local_gateway_id(&mut self) -> String {
8613        self.counters.local_gateway += 1;
8614        format!("lgw-{:08x}", self.counters.local_gateway)
8615    }
8616
8617    fn next_local_gateway_route_table_id(&mut self) -> String {
8618        self.counters.local_gateway_route_table += 1;
8619        format!("lgw-rtb-{:08x}", self.counters.local_gateway_route_table)
8620    }
8621
8622    fn next_local_gateway_route_table_virtual_interface_group_association_id(&mut self) -> String {
8623        self.counters
8624            .local_gateway_route_table_virtual_interface_group_association += 1;
8625        format!(
8626            "lgw-vif-grp-assoc-{:08x}",
8627            self.counters
8628                .local_gateway_route_table_virtual_interface_group_association
8629        )
8630    }
8631
8632    fn next_local_gateway_route_table_vpc_association_id(&mut self) -> String {
8633        self.counters.local_gateway_route_table_vpc_association += 1;
8634        format!(
8635            "lgw-vpc-assoc-{:08x}",
8636            self.counters.local_gateway_route_table_vpc_association
8637        )
8638    }
8639
8640    fn next_local_gateway_virtual_interface_id(&mut self) -> String {
8641        self.counters.local_gateway_virtual_interface += 1;
8642        format!(
8643            "lgw-vif-{:08x}",
8644            self.counters.local_gateway_virtual_interface
8645        )
8646    }
8647
8648    fn next_local_gateway_virtual_interface_group_id(&mut self) -> String {
8649        self.counters.local_gateway_virtual_interface_group += 1;
8650        format!(
8651            "lgw-vif-grp-{:08x}",
8652            self.counters.local_gateway_virtual_interface_group
8653        )
8654    }
8655
8656    /// Seeds a local gateway record (used by tests / handlers when AWS does not
8657    /// expose a `Create*` API for the parent gateway).
8658    pub fn seed_local_gateway(&mut self, owner_id: &str) -> String {
8659        let id = self.next_local_gateway_id();
8660        let lgw = LocalGateway {
8661            local_gateway_id: id.clone(),
8662            outpost_arn: format!(
8663                "arn:aws:outposts:us-east-1:{owner_id}:outpost/op-{:08x}",
8664                self.counters.local_gateway
8665            ),
8666            owner_id: owner_id.to_string(),
8667            state: "available".to_string(),
8668            tags: Tags::new(),
8669        };
8670        self.local_gateways.insert(id.clone(), lgw);
8671        id
8672    }
8673
8674    /// Seed a `LocalGatewayRouteTable` with the caller-supplied ID, also seeding a paired
8675    /// `LocalGateway` if needed. No-op when the route table already exists. Used to keep
8676    /// `CreateLocalGatewayRoute` and `CreateLocalGatewayRouteTableVpcAssociation` working when
8677    /// the caller passes a pre-existing route-table ID (real AWS pre-creates these for Outposts).
8678    pub fn seed_local_gateway_route_table_with_id(&mut self, table_id: &str, owner_id: &str) {
8679        if self.local_gateway_route_tables.contains_key(table_id) {
8680            return;
8681        }
8682        let lgw_id = table_id
8683            .strip_prefix("lgw-rtb-")
8684            .map(|suffix| format!("lgw-{suffix}"))
8685            .unwrap_or_else(|| format!("lgw-injected-{table_id}"));
8686        if !self.local_gateways.contains_key(&lgw_id) {
8687            self.local_gateways.insert(
8688                lgw_id.clone(),
8689                LocalGateway {
8690                    local_gateway_id: lgw_id.clone(),
8691                    outpost_arn: format!(
8692                        "arn:aws:outposts:us-east-1:{owner_id}:outpost/op-injected"
8693                    ),
8694                    owner_id: owner_id.to_string(),
8695                    state: "available".to_string(),
8696                    tags: Tags::new(),
8697                },
8698            );
8699        }
8700        let arn = format!("arn:aws:ec2:us-east-1:{owner_id}:local-gateway-route-table/{table_id}");
8701        self.local_gateway_route_tables.insert(
8702            table_id.to_string(),
8703            LocalGatewayRouteTable {
8704                local_gateway_route_table_id: table_id.to_string(),
8705                local_gateway_route_table_arn: arn,
8706                local_gateway_id: lgw_id,
8707                owner_id: owner_id.to_string(),
8708                state: "available".to_string(),
8709                mode: "direct-vpc-routing".to_string(),
8710                tags: Tags::new(),
8711                state_reason_code: None,
8712                state_reason_message: None,
8713            },
8714        );
8715    }
8716
8717    pub fn create_local_gateway_route_table(
8718        &mut self,
8719        local_gateway_id: &str,
8720        mode: Option<String>,
8721        owner_id: &str,
8722        tags: Tags,
8723    ) -> Result<&LocalGatewayRouteTable, Ec2Error> {
8724        if !self.local_gateways.contains_key(local_gateway_id) {
8725            return Err(Ec2Error::InvalidLocalGatewayNotFound(
8726                local_gateway_id.to_string(),
8727            ));
8728        }
8729        let id = self.next_local_gateway_route_table_id();
8730        let arn = format!("arn:aws:ec2:us-east-1:{owner_id}:local-gateway-route-table/{id}");
8731        let table = LocalGatewayRouteTable {
8732            local_gateway_route_table_id: id.clone(),
8733            local_gateway_route_table_arn: arn,
8734            local_gateway_id: local_gateway_id.to_string(),
8735            owner_id: owner_id.to_string(),
8736            state: "available".to_string(),
8737            mode: mode.unwrap_or_else(|| "direct-vpc-routing".to_string()),
8738            tags,
8739            state_reason_code: None,
8740            state_reason_message: None,
8741        };
8742        self.local_gateway_route_tables.insert(id.clone(), table);
8743        Ok(self.local_gateway_route_tables.get(&id).unwrap())
8744    }
8745
8746    pub fn delete_local_gateway_route_table(
8747        &mut self,
8748        table_id: &str,
8749    ) -> Result<LocalGatewayRouteTable, Ec2Error> {
8750        if !self.local_gateway_route_tables.contains_key(table_id) {
8751            return Err(Ec2Error::InvalidLocalGatewayRouteTableNotFound(
8752                table_id.to_string(),
8753            ));
8754        }
8755        let has_routes = self
8756            .local_gateway_routes
8757            .keys()
8758            .any(|(rid, _)| rid == table_id);
8759        if has_routes {
8760            return Err(Ec2Error::LocalGatewayRouteTableInUse(table_id.to_string()));
8761        }
8762        let mut t = self.local_gateway_route_tables.remove(table_id).unwrap();
8763        t.state = "deleting".to_string();
8764        Ok(t)
8765    }
8766
8767    #[allow(clippy::too_many_arguments)]
8768    pub fn create_local_gateway_route(
8769        &mut self,
8770        local_gateway_route_table_id: &str,
8771        destination_cidr_block: String,
8772        owner_id: &str,
8773        local_gateway_virtual_interface_group_id: Option<String>,
8774        network_interface_id: Option<String>,
8775        subnet_id: Option<String>,
8776        destination_prefix_list_id: Option<String>,
8777        coip_pool_id: Option<String>,
8778    ) -> Result<&LocalGatewayRoute, Ec2Error> {
8779        let table_arn = match self
8780            .local_gateway_route_tables
8781            .get(local_gateway_route_table_id)
8782        {
8783            Some(t) => Some(t.local_gateway_route_table_arn.clone()),
8784            None => {
8785                return Err(Ec2Error::InvalidLocalGatewayRouteTableNotFound(
8786                    local_gateway_route_table_id.to_string(),
8787                ));
8788            }
8789        };
8790        let r#type = "static".to_string();
8791        let route = LocalGatewayRoute {
8792            destination_cidr_block: destination_cidr_block.clone(),
8793            local_gateway_route_table_id: local_gateway_route_table_id.to_string(),
8794            r#type,
8795            state: "active".to_string(),
8796            local_gateway_route_table_arn: table_arn,
8797            owner_id: owner_id.to_string(),
8798            subnet_id,
8799            network_interface_id,
8800            destination_prefix_list_id,
8801            coip_pool_id,
8802            local_gateway_virtual_interface_group_id,
8803        };
8804        let key = (
8805            local_gateway_route_table_id.to_string(),
8806            destination_cidr_block,
8807        );
8808        self.local_gateway_routes.insert(key.clone(), route);
8809        Ok(self.local_gateway_routes.get(&key).unwrap())
8810    }
8811
8812    pub fn delete_local_gateway_route(
8813        &mut self,
8814        table_id: &str,
8815        destination_cidr_block: &str,
8816    ) -> Result<LocalGatewayRoute, Ec2Error> {
8817        let key = (table_id.to_string(), destination_cidr_block.to_string());
8818        let mut r = self.local_gateway_routes.remove(&key).ok_or_else(|| {
8819            Ec2Error::InvalidLocalGatewayRouteNotFound(
8820                table_id.to_string(),
8821                destination_cidr_block.to_string(),
8822            )
8823        })?;
8824        r.state = "deleted".to_string();
8825        Ok(r)
8826    }
8827
8828    pub fn modify_local_gateway_route(
8829        &mut self,
8830        table_id: &str,
8831        destination_cidr_block: &str,
8832        new_subnet_id: Option<String>,
8833        new_network_interface_id: Option<String>,
8834    ) -> Result<&LocalGatewayRoute, Ec2Error> {
8835        let key = (table_id.to_string(), destination_cidr_block.to_string());
8836        let route = self.local_gateway_routes.get_mut(&key).ok_or_else(|| {
8837            Ec2Error::InvalidLocalGatewayRouteNotFound(
8838                table_id.to_string(),
8839                destination_cidr_block.to_string(),
8840            )
8841        })?;
8842        if new_subnet_id.is_some() {
8843            route.subnet_id = new_subnet_id;
8844        }
8845        if new_network_interface_id.is_some() {
8846            route.network_interface_id = new_network_interface_id;
8847        }
8848        Ok(self.local_gateway_routes.get(&key).unwrap())
8849    }
8850
8851    pub fn create_local_gateway_virtual_interface(
8852        &mut self,
8853        local_gateway_virtual_interface_group_id: &str,
8854        vlan: i32,
8855        local_address: String,
8856        peer_address: String,
8857        peer_bgp_asn: Option<i32>,
8858        peer_bgp_asn_extended: Option<i64>,
8859        owner_id: &str,
8860        tags: Tags,
8861    ) -> Result<&LocalGatewayVirtualInterface, Ec2Error> {
8862        // Resolve LG via group.
8863        let group = self
8864            .local_gateway_virtual_interface_groups
8865            .get(local_gateway_virtual_interface_group_id)
8866            .ok_or_else(|| {
8867                Ec2Error::InvalidLocalGatewayVirtualInterfaceGroupNotFound(
8868                    local_gateway_virtual_interface_group_id.to_string(),
8869                )
8870            })?;
8871        let local_gateway_id = group.local_gateway_id.clone();
8872        let local_bgp_asn = group.local_bgp_asn;
8873        let id = self.next_local_gateway_virtual_interface_id();
8874        let arn = format!("arn:aws:ec2:us-east-1:{owner_id}:local-gateway-virtual-interface/{id}");
8875        let vif = LocalGatewayVirtualInterface {
8876            local_gateway_virtual_interface_id: id.clone(),
8877            local_gateway_id,
8878            vlan,
8879            local_address,
8880            peer_address,
8881            local_bgp_asn,
8882            peer_bgp_asn: peer_bgp_asn.unwrap_or(0),
8883            owner_id: owner_id.to_string(),
8884            tags,
8885            configuration_state: "available".to_string(),
8886            peer_bgp_asn_extended,
8887            local_gateway_virtual_interface_arn: Some(arn),
8888        };
8889        self.local_gateway_virtual_interfaces
8890            .insert(id.clone(), vif);
8891        // Link into the group.
8892        let group = self
8893            .local_gateway_virtual_interface_groups
8894            .get_mut(local_gateway_virtual_interface_group_id)
8895            .unwrap();
8896        group.local_gateway_virtual_interface_ids.push(id.clone());
8897        Ok(self.local_gateway_virtual_interfaces.get(&id).unwrap())
8898    }
8899
8900    pub fn delete_local_gateway_virtual_interface(
8901        &mut self,
8902        vif_id: &str,
8903    ) -> Result<LocalGatewayVirtualInterface, Ec2Error> {
8904        if !self.local_gateway_virtual_interfaces.contains_key(vif_id) {
8905            return Err(Ec2Error::InvalidLocalGatewayVirtualInterfaceNotFound(
8906                vif_id.to_string(),
8907            ));
8908        }
8909        // Refuse if still part of a group.
8910        if self
8911            .local_gateway_virtual_interface_groups
8912            .values()
8913            .any(|g| {
8914                g.local_gateway_virtual_interface_ids
8915                    .iter()
8916                    .any(|i| i == vif_id)
8917            })
8918        {
8919            return Err(Ec2Error::LocalGatewayVirtualInterfaceInUse(
8920                vif_id.to_string(),
8921            ));
8922        }
8923        let v = self
8924            .local_gateway_virtual_interfaces
8925            .remove(vif_id)
8926            .unwrap();
8927        Ok(v)
8928    }
8929
8930    pub fn create_local_gateway_virtual_interface_group(
8931        &mut self,
8932        local_gateway_id: &str,
8933        local_bgp_asn: i32,
8934        local_bgp_asn_extended: Option<i64>,
8935        owner_id: &str,
8936        tags: Tags,
8937    ) -> Result<&LocalGatewayVirtualInterfaceGroup, Ec2Error> {
8938        if !self.local_gateways.contains_key(local_gateway_id) {
8939            return Err(Ec2Error::InvalidLocalGatewayNotFound(
8940                local_gateway_id.to_string(),
8941            ));
8942        }
8943        let id = self.next_local_gateway_virtual_interface_group_id();
8944        let arn =
8945            format!("arn:aws:ec2:us-east-1:{owner_id}:local-gateway-virtual-interface-group/{id}");
8946        let group = LocalGatewayVirtualInterfaceGroup {
8947            local_gateway_virtual_interface_group_id: id.clone(),
8948            local_gateway_virtual_interface_ids: Vec::new(),
8949            local_gateway_id: local_gateway_id.to_string(),
8950            owner_id: owner_id.to_string(),
8951            tags,
8952            configuration_state: Some("available".to_string()),
8953            local_bgp_asn,
8954            local_bgp_asn_extended,
8955            local_gateway_virtual_interface_group_arn: Some(arn),
8956        };
8957        self.local_gateway_virtual_interface_groups
8958            .insert(id.clone(), group);
8959        Ok(self
8960            .local_gateway_virtual_interface_groups
8961            .get(&id)
8962            .unwrap())
8963    }
8964
8965    pub fn delete_local_gateway_virtual_interface_group(
8966        &mut self,
8967        group_id: &str,
8968    ) -> Result<LocalGatewayVirtualInterfaceGroup, Ec2Error> {
8969        let g = self
8970            .local_gateway_virtual_interface_groups
8971            .get(group_id)
8972            .ok_or_else(|| {
8973                Ec2Error::InvalidLocalGatewayVirtualInterfaceGroupNotFound(group_id.to_string())
8974            })?;
8975        if !g.local_gateway_virtual_interface_ids.is_empty() {
8976            return Err(Ec2Error::LocalGatewayVirtualInterfaceGroupInUse(
8977                group_id.to_string(),
8978            ));
8979        }
8980        // Refuse if a route table association references this group.
8981        if self
8982            .local_gateway_route_table_virtual_interface_group_associations
8983            .values()
8984            .any(|a| a.local_gateway_virtual_interface_group_id == group_id)
8985        {
8986            return Err(Ec2Error::LocalGatewayVirtualInterfaceGroupInUse(
8987                group_id.to_string(),
8988            ));
8989        }
8990        let g = self
8991            .local_gateway_virtual_interface_groups
8992            .remove(group_id)
8993            .unwrap();
8994        Ok(g)
8995    }
8996
8997    pub fn create_local_gateway_route_table_virtual_interface_group_association(
8998        &mut self,
8999        local_gateway_route_table_id: &str,
9000        local_gateway_virtual_interface_group_id: &str,
9001        owner_id: &str,
9002        tags: Tags,
9003    ) -> Result<&LocalGatewayRouteTableVirtualInterfaceGroupAssociation, Ec2Error> {
9004        let table = self
9005            .local_gateway_route_tables
9006            .get(local_gateway_route_table_id)
9007            .ok_or_else(|| {
9008                Ec2Error::InvalidLocalGatewayRouteTableNotFound(
9009                    local_gateway_route_table_id.to_string(),
9010                )
9011            })?;
9012        let local_gateway_id = table.local_gateway_id.clone();
9013        let local_gateway_route_table_arn = table.local_gateway_route_table_arn.clone();
9014        if !self
9015            .local_gateway_virtual_interface_groups
9016            .contains_key(local_gateway_virtual_interface_group_id)
9017        {
9018            return Err(Ec2Error::InvalidLocalGatewayVirtualInterfaceGroupNotFound(
9019                local_gateway_virtual_interface_group_id.to_string(),
9020            ));
9021        }
9022        let id = self.next_local_gateway_route_table_virtual_interface_group_association_id();
9023        let assoc = LocalGatewayRouteTableVirtualInterfaceGroupAssociation {
9024            local_gateway_route_table_virtual_interface_group_association_id: id.clone(),
9025            local_gateway_virtual_interface_group_id: local_gateway_virtual_interface_group_id
9026                .to_string(),
9027            local_gateway_route_table_id: local_gateway_route_table_id.to_string(),
9028            local_gateway_route_table_arn,
9029            local_gateway_id,
9030            owner_id: owner_id.to_string(),
9031            state: "associated".to_string(),
9032            tags,
9033        };
9034        self.local_gateway_route_table_virtual_interface_group_associations
9035            .insert(id.clone(), assoc);
9036        Ok(self
9037            .local_gateway_route_table_virtual_interface_group_associations
9038            .get(&id)
9039            .unwrap())
9040    }
9041
9042    pub fn delete_local_gateway_route_table_virtual_interface_group_association(
9043        &mut self,
9044        association_id: &str,
9045    ) -> Result<LocalGatewayRouteTableVirtualInterfaceGroupAssociation, Ec2Error> {
9046        let mut a = self
9047            .local_gateway_route_table_virtual_interface_group_associations
9048            .remove(association_id)
9049            .ok_or_else(|| {
9050                Ec2Error::InvalidLocalGatewayRouteTableVirtualInterfaceGroupAssociationNotFound(
9051                    association_id.to_string(),
9052                )
9053            })?;
9054        a.state = "disassociated".to_string();
9055        Ok(a)
9056    }
9057
9058    pub fn create_local_gateway_route_table_vpc_association(
9059        &mut self,
9060        local_gateway_route_table_id: &str,
9061        vpc_id: &str,
9062        owner_id: &str,
9063        tags: Tags,
9064    ) -> Result<&LocalGatewayRouteTableVpcAssociation, Ec2Error> {
9065        let table = self
9066            .local_gateway_route_tables
9067            .get(local_gateway_route_table_id)
9068            .ok_or_else(|| {
9069                Ec2Error::InvalidLocalGatewayRouteTableNotFound(
9070                    local_gateway_route_table_id.to_string(),
9071                )
9072            })?;
9073        let local_gateway_id = table.local_gateway_id.clone();
9074        let local_gateway_route_table_arn = table.local_gateway_route_table_arn.clone();
9075        let id = self.next_local_gateway_route_table_vpc_association_id();
9076        let assoc = LocalGatewayRouteTableVpcAssociation {
9077            local_gateway_route_table_vpc_association_id: id.clone(),
9078            local_gateway_route_table_id: local_gateway_route_table_id.to_string(),
9079            local_gateway_route_table_arn,
9080            local_gateway_id,
9081            vpc_id: vpc_id.to_string(),
9082            owner_id: owner_id.to_string(),
9083            state: "associated".to_string(),
9084            tags,
9085        };
9086        self.local_gateway_route_table_vpc_associations
9087            .insert(id.clone(), assoc);
9088        Ok(self
9089            .local_gateway_route_table_vpc_associations
9090            .get(&id)
9091            .unwrap())
9092    }
9093
9094    pub fn delete_local_gateway_route_table_vpc_association(
9095        &mut self,
9096        association_id: &str,
9097    ) -> Result<LocalGatewayRouteTableVpcAssociation, Ec2Error> {
9098        let mut a = self
9099            .local_gateway_route_table_vpc_associations
9100            .remove(association_id)
9101            .ok_or_else(|| {
9102                Ec2Error::InvalidLocalGatewayRouteTableVpcAssociationNotFound(
9103                    association_id.to_string(),
9104                )
9105            })?;
9106        a.state = "disassociated".to_string();
9107        Ok(a)
9108    }
9109
9110    // ===== Group 8: Route Server =====
9111
9112    fn next_route_server_id(&mut self) -> String {
9113        self.counters.route_server += 1;
9114        format!("rs-{:08x}", self.counters.route_server)
9115    }
9116
9117    fn next_route_server_endpoint_id(&mut self) -> String {
9118        self.counters.route_server_endpoint += 1;
9119        format!("rse-{:08x}", self.counters.route_server_endpoint)
9120    }
9121
9122    fn next_route_server_peer_id(&mut self) -> String {
9123        self.counters.route_server_peer += 1;
9124        format!("rsp-{:08x}", self.counters.route_server_peer)
9125    }
9126
9127    #[allow(clippy::too_many_arguments)]
9128    pub fn create_route_server(
9129        &mut self,
9130        amazon_side_asn: i64,
9131        persist_routes: Option<String>,
9132        persist_routes_duration: Option<i64>,
9133        sns_notifications_enabled: Option<bool>,
9134        owner_id: &str,
9135        tags: Tags,
9136    ) -> &RouteServer {
9137        let id = self.next_route_server_id();
9138        let arn = format!("arn:aws:ec2:us-east-1:{owner_id}:route-server/{id}");
9139        let rs = RouteServer {
9140            route_server_id: id.clone(),
9141            route_server_arn: arn,
9142            amazon_side_asn,
9143            state: "available".to_string(),
9144            // Action ("enable" / "disable" / "reset") -> state ("enabled" /
9145            // "disabled" / "resetting") per AWS semantics.
9146            persist_routes: action_to_persist_routes_state(persist_routes.as_deref()),
9147            persist_routes_duration,
9148            sns_notifications_enabled: sns_notifications_enabled.unwrap_or(false),
9149            sns_topic_arn: None,
9150            tags,
9151        };
9152        self.route_servers.insert(id.clone(), rs);
9153        self.route_servers.get(&id).unwrap()
9154    }
9155
9156    pub fn delete_route_server(&mut self, route_server_id: &str) -> Result<RouteServer, Ec2Error> {
9157        if !self.route_servers.contains_key(route_server_id) {
9158            return Err(Ec2Error::InvalidRouteServerNotFound(
9159                route_server_id.to_string(),
9160            ));
9161        }
9162        let has_endpoints = self
9163            .route_server_endpoints
9164            .values()
9165            .any(|e| e.route_server_id == route_server_id);
9166        let has_associations = self
9167            .route_server_associations
9168            .keys()
9169            .any(|(rsid, _)| rsid == route_server_id);
9170        if has_endpoints || has_associations {
9171            return Err(Ec2Error::RouteServerInUse(route_server_id.to_string()));
9172        }
9173        let mut rs = self.route_servers.remove(route_server_id).unwrap();
9174        rs.state = "deleting".to_string();
9175        Ok(rs)
9176    }
9177
9178    pub fn modify_route_server(
9179        &mut self,
9180        route_server_id: &str,
9181        persist_routes: Option<String>,
9182        persist_routes_duration: Option<i64>,
9183        sns_notifications_enabled: Option<bool>,
9184    ) -> Result<&RouteServer, Ec2Error> {
9185        let rs = self
9186            .route_servers
9187            .get_mut(route_server_id)
9188            .ok_or_else(|| Ec2Error::InvalidRouteServerNotFound(route_server_id.to_string()))?;
9189        if let Some(p) = persist_routes {
9190            rs.persist_routes = action_to_persist_routes_state(Some(p.as_str()));
9191        }
9192        if let Some(d) = persist_routes_duration {
9193            rs.persist_routes_duration = Some(d);
9194        }
9195        if let Some(e) = sns_notifications_enabled {
9196            rs.sns_notifications_enabled = e;
9197        }
9198        Ok(self.route_servers.get(route_server_id).unwrap())
9199    }
9200
9201    pub fn associate_route_server(
9202        &mut self,
9203        route_server_id: &str,
9204        vpc_id: &str,
9205    ) -> Result<&RouteServerAssociation, Ec2Error> {
9206        if !self.route_servers.contains_key(route_server_id) {
9207            return Err(Ec2Error::InvalidRouteServerNotFound(
9208                route_server_id.to_string(),
9209            ));
9210        }
9211        if !self.vpcs.contains_key(vpc_id) {
9212            return Err(Ec2Error::VpcNotFound(vpc_id.to_string()));
9213        }
9214        let key = (route_server_id.to_string(), vpc_id.to_string());
9215        let assoc = RouteServerAssociation {
9216            route_server_id: route_server_id.to_string(),
9217            vpc_id: vpc_id.to_string(),
9218            state: "associated".to_string(),
9219            propagations: Vec::new(),
9220        };
9221        self.route_server_associations.insert(key.clone(), assoc);
9222        Ok(self.route_server_associations.get(&key).unwrap())
9223    }
9224
9225    pub fn disassociate_route_server(
9226        &mut self,
9227        route_server_id: &str,
9228        vpc_id: &str,
9229    ) -> Result<RouteServerAssociation, Ec2Error> {
9230        if !self.route_servers.contains_key(route_server_id) {
9231            return Err(Ec2Error::InvalidRouteServerNotFound(
9232                route_server_id.to_string(),
9233            ));
9234        }
9235        let key = (route_server_id.to_string(), vpc_id.to_string());
9236        let mut a = self.route_server_associations.remove(&key).ok_or_else(|| {
9237            Ec2Error::InvalidRouteServerAssociationNotFound(
9238                route_server_id.to_string(),
9239                vpc_id.to_string(),
9240            )
9241        })?;
9242        a.state = "disassociated".to_string();
9243        Ok(a)
9244    }
9245
9246    pub fn create_route_server_endpoint(
9247        &mut self,
9248        route_server_id: &str,
9249        subnet_id: &str,
9250        tags: Tags,
9251    ) -> Result<&RouteServerEndpoint, Ec2Error> {
9252        if !self.route_servers.contains_key(route_server_id) {
9253            return Err(Ec2Error::InvalidRouteServerNotFound(
9254                route_server_id.to_string(),
9255            ));
9256        }
9257        let vpc_id = self
9258            .subnets
9259            .get(subnet_id)
9260            .map(|s| s.vpc_id.clone())
9261            .unwrap_or_default();
9262        let id = self.next_route_server_endpoint_id();
9263        // Synthesise an ENI ID (we don't materialise an actual NetworkInterface;
9264        // that would require copying the existing eni_to_model machinery and
9265        // is not exercised by any of the Route-Server-only ops in this batch).
9266        let eni_id = format!("eni-rse{:08x}", self.counters.route_server_endpoint);
9267        let endpoint = RouteServerEndpoint {
9268            route_server_endpoint_id: id.clone(),
9269            route_server_id: route_server_id.to_string(),
9270            vpc_id,
9271            subnet_id: subnet_id.to_string(),
9272            eni_id,
9273            eni_address: None,
9274            state: "available".to_string(),
9275            failure_reason: None,
9276            tags,
9277        };
9278        self.route_server_endpoints.insert(id.clone(), endpoint);
9279        Ok(self.route_server_endpoints.get(&id).unwrap())
9280    }
9281
9282    pub fn delete_route_server_endpoint(
9283        &mut self,
9284        endpoint_id: &str,
9285    ) -> Result<RouteServerEndpoint, Ec2Error> {
9286        if !self.route_server_endpoints.contains_key(endpoint_id) {
9287            return Err(Ec2Error::InvalidRouteServerEndpointNotFound(
9288                endpoint_id.to_string(),
9289            ));
9290        }
9291        let has_peers = self
9292            .route_server_peers
9293            .values()
9294            .any(|p| p.route_server_endpoint_id == endpoint_id);
9295        if has_peers {
9296            return Err(Ec2Error::RouteServerEndpointInUse(endpoint_id.to_string()));
9297        }
9298        let mut ep = self.route_server_endpoints.remove(endpoint_id).unwrap();
9299        ep.state = "deleting".to_string();
9300        Ok(ep)
9301    }
9302
9303    pub fn create_route_server_peer(
9304        &mut self,
9305        endpoint_id: &str,
9306        peer_address: String,
9307        peer_asn: i64,
9308        peer_liveness_detection: Option<String>,
9309        tags: Tags,
9310    ) -> Result<&RouteServerPeer, Ec2Error> {
9311        let endpoint = self
9312            .route_server_endpoints
9313            .get(endpoint_id)
9314            .ok_or_else(|| Ec2Error::InvalidRouteServerEndpointNotFound(endpoint_id.to_string()))?;
9315        let route_server_id = endpoint.route_server_id.clone();
9316        let vpc_id = endpoint.vpc_id.clone();
9317        let subnet_id = endpoint.subnet_id.clone();
9318        let endpoint_eni_id = Some(endpoint.eni_id.clone());
9319        let endpoint_eni_address = endpoint.eni_address.clone();
9320        let id = self.next_route_server_peer_id();
9321        let liveness = peer_liveness_detection.unwrap_or_else(|| "bgp-keepalive".to_string());
9322        let bgp_options = RouteServerBgpOptions {
9323            peer_asn: Some(peer_asn),
9324            peer_liveness_detection: Some(liveness.clone()),
9325        };
9326        let options = RouteServerPeerOptions {
9327            peer_asn,
9328            peer_liveness_detection: liveness,
9329            bgp_options: Some(bgp_options),
9330        };
9331        let peer = RouteServerPeer {
9332            route_server_peer_id: id.clone(),
9333            route_server_endpoint_id: endpoint_id.to_string(),
9334            route_server_id,
9335            vpc_id,
9336            subnet_id,
9337            peer_address,
9338            state: "available".to_string(),
9339            failure_reason: None,
9340            options,
9341            endpoint_eni_id,
9342            endpoint_eni_address,
9343            tags,
9344        };
9345        self.route_server_peers.insert(id.clone(), peer);
9346        Ok(self.route_server_peers.get(&id).unwrap())
9347    }
9348
9349    pub fn delete_route_server_peer(&mut self, peer_id: &str) -> Result<RouteServerPeer, Ec2Error> {
9350        let mut p = self
9351            .route_server_peers
9352            .remove(peer_id)
9353            .ok_or_else(|| Ec2Error::InvalidRouteServerPeerNotFound(peer_id.to_string()))?;
9354        p.state = "deleting".to_string();
9355        Ok(p)
9356    }
9357
9358    // ===== Group 9: Verified Access =====
9359
9360    fn next_verified_access_instance_id(&mut self) -> String {
9361        self.counters.verified_access_instance += 1;
9362        format!("vai-{:08x}", self.counters.verified_access_instance)
9363    }
9364
9365    fn next_verified_access_trust_provider_id(&mut self) -> String {
9366        self.counters.verified_access_trust_provider += 1;
9367        format!("vatp-{:08x}", self.counters.verified_access_trust_provider)
9368    }
9369
9370    fn next_verified_access_group_id(&mut self) -> String {
9371        self.counters.verified_access_group += 1;
9372        format!("vagr-{:08x}", self.counters.verified_access_group)
9373    }
9374
9375    fn next_verified_access_endpoint_id(&mut self) -> String {
9376        self.counters.verified_access_endpoint += 1;
9377        format!("vae-{:08x}", self.counters.verified_access_endpoint)
9378    }
9379
9380    pub fn create_verified_access_instance(
9381        &mut self,
9382        description: Option<String>,
9383        fips_enabled: bool,
9384        cidr_endpoints_custom_subdomain: Option<String>,
9385        name: Option<String>,
9386        tags: Tags,
9387    ) -> &VerifiedAccessInstance {
9388        let id = self.next_verified_access_instance_id();
9389        let now = self.now_iso();
9390        let inst = VerifiedAccessInstance {
9391            verified_access_instance_id: id.clone(),
9392            description,
9393            creation_time: now.clone(),
9394            last_updated_time: now,
9395            fips_enabled,
9396            cidr_endpoints_custom_subdomain,
9397            name,
9398            trust_provider_ids: Vec::new(),
9399            tags,
9400        };
9401        self.verified_access_instances.insert(id.clone(), inst);
9402        self.verified_access_instances.get(&id).unwrap()
9403    }
9404
9405    #[allow(clippy::too_many_arguments)]
9406    pub fn create_verified_access_trust_provider(
9407        &mut self,
9408        description: Option<String>,
9409        trust_provider_type: String,
9410        user_trust_provider_type: Option<String>,
9411        device_trust_provider_type: Option<String>,
9412        oidc_options: Option<VerifiedAccessOidcOptions>,
9413        device_options: Option<VerifiedAccessDeviceOptions>,
9414        native_application_oidc_options: Option<VerifiedAccessNativeApplicationOidcOptions>,
9415        policy_reference_name: String,
9416        sse_specification: VerifiedAccessSseSpecification,
9417        tags: Tags,
9418    ) -> &VerifiedAccessTrustProvider {
9419        let id = self.next_verified_access_trust_provider_id();
9420        let now = self.now_iso();
9421        let tp = VerifiedAccessTrustProvider {
9422            verified_access_trust_provider_id: id.clone(),
9423            description,
9424            trust_provider_type,
9425            user_trust_provider_type,
9426            device_trust_provider_type,
9427            oidc_options,
9428            device_options,
9429            native_application_oidc_options,
9430            policy_reference_name,
9431            creation_time: now.clone(),
9432            last_updated_time: now,
9433            sse_specification,
9434            tags,
9435        };
9436        self.verified_access_trust_providers.insert(id.clone(), tp);
9437        self.verified_access_trust_providers.get(&id).unwrap()
9438    }
9439
9440    pub fn create_verified_access_group(
9441        &mut self,
9442        instance_id: String,
9443        owner: String,
9444        description: Option<String>,
9445        sse_specification: VerifiedAccessSseSpecification,
9446        policy_document: Option<String>,
9447        tags: Tags,
9448    ) -> Result<&VerifiedAccessGroup, Ec2Error> {
9449        if !self.verified_access_instances.contains_key(&instance_id) {
9450            return Err(Ec2Error::InvalidVerifiedAccessInstanceNotFound(
9451                instance_id.clone(),
9452            ));
9453        }
9454        let id = self.next_verified_access_group_id();
9455        let arn = format!(
9456            "arn:aws:ec2:us-east-1:{owner}:verified-access-group/{id}",
9457            owner = owner,
9458            id = id,
9459        );
9460        let now = self.now_iso();
9461        let policy_enabled = policy_document.is_some();
9462        let group = VerifiedAccessGroup {
9463            verified_access_group_id: id.clone(),
9464            verified_access_group_arn: arn,
9465            verified_access_instance_id: instance_id,
9466            owner,
9467            description,
9468            creation_time: now.clone(),
9469            last_updated_time: now,
9470            deletion_time: None,
9471            sse_specification,
9472            policy_document,
9473            policy_enabled,
9474            tags,
9475        };
9476        self.verified_access_groups.insert(id.clone(), group);
9477        Ok(self.verified_access_groups.get(&id).unwrap())
9478    }
9479
9480    #[allow(clippy::too_many_arguments)]
9481    pub fn create_verified_access_endpoint(
9482        &mut self,
9483        verified_access_group_id: String,
9484        application_domain: Option<String>,
9485        endpoint_type: String,
9486        attachment_type: String,
9487        domain_certificate_arn: Option<String>,
9488        endpoint_domain_prefix: Option<String>,
9489        security_group_ids: Vec<String>,
9490        load_balancer_options: Option<VerifiedAccessEndpointLoadBalancerOptions>,
9491        network_interface_options: Option<VerifiedAccessEndpointEniOptions>,
9492        cidr_options: Option<VerifiedAccessEndpointCidrOptions>,
9493        rds_options: Option<VerifiedAccessEndpointRdsOptions>,
9494        description: Option<String>,
9495        policy_document: Option<String>,
9496        sse_specification: VerifiedAccessSseSpecification,
9497        tags: Tags,
9498    ) -> Result<&VerifiedAccessEndpoint, Ec2Error> {
9499        let group = self
9500            .verified_access_groups
9501            .get(&verified_access_group_id)
9502            .ok_or_else(|| {
9503                Ec2Error::InvalidVerifiedAccessGroupNotFound(verified_access_group_id.clone())
9504            })?;
9505        let instance_id = group.verified_access_instance_id.clone();
9506        let id = self.next_verified_access_endpoint_id();
9507        let now = self.now_iso();
9508        let endpoint_domain = match (
9509            endpoint_domain_prefix.as_deref(),
9510            application_domain.as_deref(),
9511        ) {
9512            (Some(p), Some(d)) => Some(format!("{p}.{d}")),
9513            (Some(p), None) => Some(p.to_string()),
9514            (None, Some(d)) => Some(d.to_string()),
9515            (None, None) => None,
9516        };
9517        let policy_enabled = policy_document.is_some();
9518        let ep = VerifiedAccessEndpoint {
9519            verified_access_endpoint_id: id.clone(),
9520            verified_access_instance_id: instance_id,
9521            verified_access_group_id,
9522            application_domain,
9523            endpoint_type,
9524            attachment_type,
9525            domain_certificate_arn,
9526            endpoint_domain,
9527            device_validation_domain: None,
9528            security_group_ids,
9529            load_balancer_options,
9530            network_interface_options,
9531            cidr_options,
9532            rds_options,
9533            status_code: "active".to_string(),
9534            status_message: None,
9535            description,
9536            creation_time: now.clone(),
9537            last_updated_time: now,
9538            deletion_time: None,
9539            sse_specification,
9540            policy_document,
9541            policy_enabled,
9542            tags,
9543        };
9544        self.verified_access_endpoints.insert(id.clone(), ep);
9545        Ok(self.verified_access_endpoints.get(&id).unwrap())
9546    }
9547
9548    pub fn attach_verified_access_trust_provider(
9549        &mut self,
9550        instance_id: &str,
9551        trust_provider_id: &str,
9552    ) -> Result<(VerifiedAccessTrustProvider, VerifiedAccessInstance), Ec2Error> {
9553        if !self.verified_access_instances.contains_key(instance_id) {
9554            return Err(Ec2Error::InvalidVerifiedAccessInstanceNotFound(
9555                instance_id.to_string(),
9556            ));
9557        }
9558        if !self
9559            .verified_access_trust_providers
9560            .contains_key(trust_provider_id)
9561        {
9562            return Err(Ec2Error::InvalidVerifiedAccessTrustProviderNotFound(
9563                trust_provider_id.to_string(),
9564            ));
9565        }
9566        let key = (instance_id.to_string(), trust_provider_id.to_string());
9567        self.verified_access_trust_provider_attachments.insert(
9568            key,
9569            VerifiedAccessTrustProviderAttachment {
9570                instance_id: instance_id.to_string(),
9571                trust_provider_id: trust_provider_id.to_string(),
9572            },
9573        );
9574        // Update instance to include the trust provider id (deduped) and bump
9575        // last_updated_time.
9576        let now = self.now_iso();
9577        if let Some(inst) = self.verified_access_instances.get_mut(instance_id) {
9578            if !inst
9579                .trust_provider_ids
9580                .iter()
9581                .any(|x| x == trust_provider_id)
9582            {
9583                inst.trust_provider_ids.push(trust_provider_id.to_string());
9584            }
9585            inst.last_updated_time = now.clone();
9586        }
9587        if let Some(tp) = self
9588            .verified_access_trust_providers
9589            .get_mut(trust_provider_id)
9590        {
9591            tp.last_updated_time = now;
9592        }
9593        let inst = self
9594            .verified_access_instances
9595            .get(instance_id)
9596            .unwrap()
9597            .clone();
9598        let tp = self
9599            .verified_access_trust_providers
9600            .get(trust_provider_id)
9601            .unwrap()
9602            .clone();
9603        Ok((tp, inst))
9604    }
9605
9606    pub fn detach_verified_access_trust_provider(
9607        &mut self,
9608        instance_id: &str,
9609        trust_provider_id: &str,
9610    ) -> Result<(VerifiedAccessTrustProvider, VerifiedAccessInstance), Ec2Error> {
9611        if !self.verified_access_instances.contains_key(instance_id) {
9612            return Err(Ec2Error::InvalidVerifiedAccessInstanceNotFound(
9613                instance_id.to_string(),
9614            ));
9615        }
9616        if !self
9617            .verified_access_trust_providers
9618            .contains_key(trust_provider_id)
9619        {
9620            return Err(Ec2Error::InvalidVerifiedAccessTrustProviderNotFound(
9621                trust_provider_id.to_string(),
9622            ));
9623        }
9624        let key = (instance_id.to_string(), trust_provider_id.to_string());
9625        if self
9626            .verified_access_trust_provider_attachments
9627            .remove(&key)
9628            .is_none()
9629        {
9630            return Err(
9631                Ec2Error::InvalidVerifiedAccessTrustProviderAttachmentNotFound(
9632                    instance_id.to_string(),
9633                    trust_provider_id.to_string(),
9634                ),
9635            );
9636        }
9637        let now = self.now_iso();
9638        if let Some(inst) = self.verified_access_instances.get_mut(instance_id) {
9639            inst.trust_provider_ids.retain(|x| x != trust_provider_id);
9640            inst.last_updated_time = now.clone();
9641        }
9642        if let Some(tp) = self
9643            .verified_access_trust_providers
9644            .get_mut(trust_provider_id)
9645        {
9646            tp.last_updated_time = now;
9647        }
9648        let inst = self
9649            .verified_access_instances
9650            .get(instance_id)
9651            .unwrap()
9652            .clone();
9653        let tp = self
9654            .verified_access_trust_providers
9655            .get(trust_provider_id)
9656            .unwrap()
9657            .clone();
9658        Ok((tp, inst))
9659    }
9660
9661    pub fn delete_verified_access_endpoint(
9662        &mut self,
9663        endpoint_id: &str,
9664    ) -> Result<VerifiedAccessEndpoint, Ec2Error> {
9665        let mut ep = self
9666            .verified_access_endpoints
9667            .remove(endpoint_id)
9668            .ok_or_else(|| {
9669                Ec2Error::InvalidVerifiedAccessEndpointNotFound(endpoint_id.to_string())
9670            })?;
9671        let now = self.now_iso();
9672        ep.status_code = "deleted".to_string();
9673        ep.deletion_time = Some(now.clone());
9674        ep.last_updated_time = now;
9675        Ok(ep)
9676    }
9677
9678    pub fn delete_verified_access_group(
9679        &mut self,
9680        group_id: &str,
9681    ) -> Result<VerifiedAccessGroup, Ec2Error> {
9682        if !self.verified_access_groups.contains_key(group_id) {
9683            return Err(Ec2Error::InvalidVerifiedAccessGroupNotFound(
9684                group_id.to_string(),
9685            ));
9686        }
9687        let in_use = self
9688            .verified_access_endpoints
9689            .values()
9690            .any(|e| e.verified_access_group_id == group_id);
9691        if in_use {
9692            return Err(Ec2Error::VerifiedAccessGroupInUse(group_id.to_string()));
9693        }
9694        let mut group = self.verified_access_groups.remove(group_id).unwrap();
9695        let now = self.now_iso();
9696        group.deletion_time = Some(now.clone());
9697        group.last_updated_time = now;
9698        Ok(group)
9699    }
9700
9701    pub fn delete_verified_access_instance(
9702        &mut self,
9703        instance_id: &str,
9704    ) -> Result<VerifiedAccessInstance, Ec2Error> {
9705        if !self.verified_access_instances.contains_key(instance_id) {
9706            return Err(Ec2Error::InvalidVerifiedAccessInstanceNotFound(
9707                instance_id.to_string(),
9708            ));
9709        }
9710        let has_attachments = self
9711            .verified_access_trust_provider_attachments
9712            .keys()
9713            .any(|(iid, _)| iid == instance_id);
9714        let has_groups = self
9715            .verified_access_groups
9716            .values()
9717            .any(|g| g.verified_access_instance_id == instance_id);
9718        if has_attachments || has_groups {
9719            return Err(Ec2Error::VerifiedAccessInstanceInUse(
9720                instance_id.to_string(),
9721            ));
9722        }
9723        // Remove logging configuration alongside the instance.
9724        self.verified_access_instance_logging_configurations
9725            .remove(instance_id);
9726        Ok(self.verified_access_instances.remove(instance_id).unwrap())
9727    }
9728
9729    pub fn delete_verified_access_trust_provider(
9730        &mut self,
9731        trust_provider_id: &str,
9732    ) -> Result<VerifiedAccessTrustProvider, Ec2Error> {
9733        if !self
9734            .verified_access_trust_providers
9735            .contains_key(trust_provider_id)
9736        {
9737            return Err(Ec2Error::InvalidVerifiedAccessTrustProviderNotFound(
9738                trust_provider_id.to_string(),
9739            ));
9740        }
9741        let attached = self
9742            .verified_access_trust_provider_attachments
9743            .keys()
9744            .any(|(_, tp)| tp == trust_provider_id);
9745        if attached {
9746            return Err(Ec2Error::VerifiedAccessTrustProviderInUse(
9747                trust_provider_id.to_string(),
9748            ));
9749        }
9750        Ok(self
9751            .verified_access_trust_providers
9752            .remove(trust_provider_id)
9753            .unwrap())
9754    }
9755
9756    pub fn modify_verified_access_instance(
9757        &mut self,
9758        instance_id: &str,
9759        description: Option<String>,
9760        cidr_endpoints_custom_subdomain: Option<String>,
9761    ) -> Result<&VerifiedAccessInstance, Ec2Error> {
9762        let now = self.now_iso();
9763        let inst = self
9764            .verified_access_instances
9765            .get_mut(instance_id)
9766            .ok_or_else(|| {
9767                Ec2Error::InvalidVerifiedAccessInstanceNotFound(instance_id.to_string())
9768            })?;
9769        if let Some(d) = description {
9770            inst.description = Some(d);
9771        }
9772        if let Some(s) = cidr_endpoints_custom_subdomain {
9773            inst.cidr_endpoints_custom_subdomain = Some(s);
9774        }
9775        inst.last_updated_time = now;
9776        Ok(self.verified_access_instances.get(instance_id).unwrap())
9777    }
9778
9779    #[allow(clippy::too_many_arguments)]
9780    pub fn modify_verified_access_trust_provider(
9781        &mut self,
9782        trust_provider_id: &str,
9783        description: Option<String>,
9784        oidc_options: Option<VerifiedAccessOidcOptions>,
9785        device_options: Option<VerifiedAccessDeviceOptions>,
9786        sse_specification: Option<VerifiedAccessSseSpecification>,
9787    ) -> Result<&VerifiedAccessTrustProvider, Ec2Error> {
9788        let now = self.now_iso();
9789        let tp = self
9790            .verified_access_trust_providers
9791            .get_mut(trust_provider_id)
9792            .ok_or_else(|| {
9793                Ec2Error::InvalidVerifiedAccessTrustProviderNotFound(trust_provider_id.to_string())
9794            })?;
9795        if let Some(d) = description {
9796            tp.description = Some(d);
9797        }
9798        if let Some(o) = oidc_options {
9799            // Merge non-None fields into existing oidc_options.
9800            let mut existing = tp.oidc_options.clone().unwrap_or_default();
9801            if o.issuer.is_some() {
9802                existing.issuer = o.issuer;
9803            }
9804            if o.authorization_endpoint.is_some() {
9805                existing.authorization_endpoint = o.authorization_endpoint;
9806            }
9807            if o.token_endpoint.is_some() {
9808                existing.token_endpoint = o.token_endpoint;
9809            }
9810            if o.user_info_endpoint.is_some() {
9811                existing.user_info_endpoint = o.user_info_endpoint;
9812            }
9813            if o.client_id.is_some() {
9814                existing.client_id = o.client_id;
9815            }
9816            if o.client_secret.is_some() {
9817                existing.client_secret = o.client_secret;
9818            }
9819            if o.scope.is_some() {
9820                existing.scope = o.scope;
9821            }
9822            tp.oidc_options = Some(existing);
9823        }
9824        if let Some(d) = device_options {
9825            let mut existing = tp.device_options.clone().unwrap_or_default();
9826            if d.public_signing_key_url.is_some() {
9827                existing.public_signing_key_url = d.public_signing_key_url;
9828            }
9829            tp.device_options = Some(existing);
9830        }
9831        if let Some(s) = sse_specification {
9832            tp.sse_specification = s;
9833        }
9834        tp.last_updated_time = now;
9835        Ok(self
9836            .verified_access_trust_providers
9837            .get(trust_provider_id)
9838            .unwrap())
9839    }
9840
9841    pub fn modify_verified_access_group(
9842        &mut self,
9843        group_id: &str,
9844        description: Option<String>,
9845        verified_access_instance_id: Option<String>,
9846    ) -> Result<&VerifiedAccessGroup, Ec2Error> {
9847        if let Some(ref iid) = verified_access_instance_id {
9848            if !self.verified_access_instances.contains_key(iid) {
9849                return Err(Ec2Error::InvalidVerifiedAccessInstanceNotFound(iid.clone()));
9850            }
9851        }
9852        let now = self.now_iso();
9853        let group = self
9854            .verified_access_groups
9855            .get_mut(group_id)
9856            .ok_or_else(|| Ec2Error::InvalidVerifiedAccessGroupNotFound(group_id.to_string()))?;
9857        if let Some(d) = description {
9858            group.description = Some(d);
9859        }
9860        if let Some(iid) = verified_access_instance_id {
9861            group.verified_access_instance_id = iid;
9862        }
9863        group.last_updated_time = now;
9864        Ok(self.verified_access_groups.get(group_id).unwrap())
9865    }
9866
9867    pub fn modify_verified_access_group_policy(
9868        &mut self,
9869        group_id: &str,
9870        policy_document: Option<String>,
9871        policy_enabled: Option<bool>,
9872        sse_specification: Option<VerifiedAccessSseSpecification>,
9873    ) -> Result<&VerifiedAccessGroup, Ec2Error> {
9874        let now = self.now_iso();
9875        let group = self
9876            .verified_access_groups
9877            .get_mut(group_id)
9878            .ok_or_else(|| Ec2Error::InvalidVerifiedAccessGroupNotFound(group_id.to_string()))?;
9879        if let Some(d) = policy_document {
9880            group.policy_document = Some(d);
9881        }
9882        if let Some(b) = policy_enabled {
9883            group.policy_enabled = b;
9884        }
9885        if let Some(s) = sse_specification {
9886            group.sse_specification = s;
9887        }
9888        group.last_updated_time = now;
9889        Ok(self.verified_access_groups.get(group_id).unwrap())
9890    }
9891
9892    #[allow(clippy::too_many_arguments)]
9893    pub fn modify_verified_access_endpoint(
9894        &mut self,
9895        endpoint_id: &str,
9896        description: Option<String>,
9897        verified_access_group_id: Option<String>,
9898        load_balancer_options: Option<VerifiedAccessEndpointLoadBalancerOptions>,
9899        network_interface_options: Option<VerifiedAccessEndpointEniOptions>,
9900        cidr_options: Option<VerifiedAccessEndpointCidrOptions>,
9901        rds_options: Option<VerifiedAccessEndpointRdsOptions>,
9902    ) -> Result<&VerifiedAccessEndpoint, Ec2Error> {
9903        if let Some(ref gid) = verified_access_group_id {
9904            if !self.verified_access_groups.contains_key(gid) {
9905                return Err(Ec2Error::InvalidVerifiedAccessGroupNotFound(gid.clone()));
9906            }
9907        }
9908        let now = self.now_iso();
9909        let ep = self
9910            .verified_access_endpoints
9911            .get_mut(endpoint_id)
9912            .ok_or_else(|| {
9913                Ec2Error::InvalidVerifiedAccessEndpointNotFound(endpoint_id.to_string())
9914            })?;
9915        if let Some(d) = description {
9916            ep.description = Some(d);
9917        }
9918        if let Some(gid) = verified_access_group_id {
9919            ep.verified_access_group_id = gid;
9920        }
9921        if let Some(o) = load_balancer_options {
9922            ep.load_balancer_options = Some(o);
9923        }
9924        if let Some(o) = network_interface_options {
9925            ep.network_interface_options = Some(o);
9926        }
9927        if let Some(o) = cidr_options {
9928            ep.cidr_options = Some(o);
9929        }
9930        if let Some(o) = rds_options {
9931            ep.rds_options = Some(o);
9932        }
9933        ep.last_updated_time = now;
9934        Ok(self.verified_access_endpoints.get(endpoint_id).unwrap())
9935    }
9936
9937    pub fn modify_verified_access_endpoint_policy(
9938        &mut self,
9939        endpoint_id: &str,
9940        policy_document: Option<String>,
9941        policy_enabled: Option<bool>,
9942        sse_specification: Option<VerifiedAccessSseSpecification>,
9943    ) -> Result<&VerifiedAccessEndpoint, Ec2Error> {
9944        let now = self.now_iso();
9945        let ep = self
9946            .verified_access_endpoints
9947            .get_mut(endpoint_id)
9948            .ok_or_else(|| {
9949                Ec2Error::InvalidVerifiedAccessEndpointNotFound(endpoint_id.to_string())
9950            })?;
9951        if let Some(d) = policy_document {
9952            ep.policy_document = Some(d);
9953        }
9954        if let Some(b) = policy_enabled {
9955            ep.policy_enabled = b;
9956        }
9957        if let Some(s) = sse_specification {
9958            ep.sse_specification = s;
9959        }
9960        ep.last_updated_time = now;
9961        Ok(self.verified_access_endpoints.get(endpoint_id).unwrap())
9962    }
9963
9964    pub fn modify_verified_access_instance_logging_configuration(
9965        &mut self,
9966        instance_id: &str,
9967        logs: VerifiedAccessLogs,
9968    ) -> Result<&VerifiedAccessLogs, Ec2Error> {
9969        if !self.verified_access_instances.contains_key(instance_id) {
9970            return Err(Ec2Error::InvalidVerifiedAccessInstanceNotFound(
9971                instance_id.to_string(),
9972            ));
9973        }
9974        self.verified_access_instance_logging_configurations
9975            .insert(instance_id.to_string(), logs);
9976        // Bump instance last_updated_time.
9977        let now = self.now_iso();
9978        if let Some(inst) = self.verified_access_instances.get_mut(instance_id) {
9979            inst.last_updated_time = now;
9980        }
9981        Ok(self
9982            .verified_access_instance_logging_configurations
9983            .get(instance_id)
9984            .unwrap())
9985    }
9986
9987    // ===== Group 10 helpers and operations =====
9988
9989    fn next_capacity_manager_data_export_id(&mut self) -> String {
9990        self.counters.capacity_manager_data_export += 1;
9991        format!("cmde-{:08x}", self.counters.capacity_manager_data_export)
9992    }
9993
9994    fn next_interruptible_capacity_reservation_allocation_id(&mut self) -> String {
9995        self.counters.interruptible_capacity_reservation_allocation += 1;
9996        format!(
9997            "icra-{:08x}",
9998            self.counters.interruptible_capacity_reservation_allocation
9999        )
10000    }
10001
10002    fn next_capacity_block_id(&mut self) -> String {
10003        self.counters.capacity_block += 1;
10004        format!("cb-{:08x}", self.counters.capacity_block)
10005    }
10006
10007    fn next_capacity_block_extension_id(&mut self) -> String {
10008        self.counters.capacity_block_extension += 1;
10009        format!("cbe-{:08x}", self.counters.capacity_block_extension)
10010    }
10011
10012    /// Create a pending billing-ownership transfer offer.
10013    pub fn associate_capacity_reservation_billing_owner(
10014        &mut self,
10015        capacity_reservation_id: &str,
10016        unused_reservation_billing_owner_id: &str,
10017        requester_account_id: &str,
10018    ) -> Result<&BillingOwnershipOffer, Ec2Error> {
10019        if !self
10020            .capacity_reservations
10021            .contains_key(capacity_reservation_id)
10022        {
10023            return Err(Ec2Error::CapacityReservationNotFound(
10024                capacity_reservation_id.to_string(),
10025            ));
10026        }
10027        let key = (
10028            capacity_reservation_id.to_string(),
10029            unused_reservation_billing_owner_id.to_string(),
10030        );
10031        if let Some(existing) = self.billing_ownership_offers.get(&key)
10032            && existing.status == "pending"
10033        {
10034            return Err(Ec2Error::BillingOwnershipOfferAlreadyExists(
10035                capacity_reservation_id.to_string(),
10036                unused_reservation_billing_owner_id.to_string(),
10037            ));
10038        }
10039        let now = self.now_iso();
10040        let offer = BillingOwnershipOffer {
10041            capacity_reservation_id: capacity_reservation_id.to_string(),
10042            unused_reservation_billing_owner_id: unused_reservation_billing_owner_id.to_string(),
10043            requested_by: requester_account_id.to_string(),
10044            status: "pending".to_string(),
10045            status_message: None,
10046            last_update_time: now,
10047        };
10048        if let Some(cr) = self.capacity_reservations.get_mut(capacity_reservation_id) {
10049            cr.pending_billing_owner_account_id =
10050                Some(unused_reservation_billing_owner_id.to_string());
10051        }
10052        self.billing_ownership_offers.insert(key.clone(), offer);
10053        Ok(self.billing_ownership_offers.get(&key).unwrap())
10054    }
10055
10056    /// Accept a pending billing-ownership offer.
10057    pub fn accept_capacity_reservation_billing_ownership(
10058        &mut self,
10059        capacity_reservation_id: &str,
10060        accepter_account_id: &str,
10061    ) -> Result<&BillingOwnershipOffer, Ec2Error> {
10062        let key = (
10063            capacity_reservation_id.to_string(),
10064            accepter_account_id.to_string(),
10065        );
10066        let now = self.now_iso();
10067        let offer = self.billing_ownership_offers.get_mut(&key).ok_or_else(|| {
10068            Ec2Error::InvalidBillingOwnershipOfferNotFound(
10069                capacity_reservation_id.to_string(),
10070                accepter_account_id.to_string(),
10071            )
10072        })?;
10073        offer.status = "accepted".to_string();
10074        offer.last_update_time = now;
10075        if let Some(cr) = self.capacity_reservations.get_mut(capacity_reservation_id) {
10076            cr.billing_owner_account_id = Some(accepter_account_id.to_string());
10077            cr.pending_billing_owner_account_id = None;
10078        }
10079        Ok(self.billing_ownership_offers.get(&key).unwrap())
10080    }
10081
10082    /// Cancel a pending billing-ownership offer (initiator side).
10083    pub fn disassociate_capacity_reservation_billing_owner(
10084        &mut self,
10085        capacity_reservation_id: &str,
10086        unused_reservation_billing_owner_id: &str,
10087    ) -> Result<&BillingOwnershipOffer, Ec2Error> {
10088        let key = (
10089            capacity_reservation_id.to_string(),
10090            unused_reservation_billing_owner_id.to_string(),
10091        );
10092        let now = self.now_iso();
10093        let offer = self.billing_ownership_offers.get_mut(&key).ok_or_else(|| {
10094            Ec2Error::InvalidBillingOwnershipOfferNotFound(
10095                capacity_reservation_id.to_string(),
10096                unused_reservation_billing_owner_id.to_string(),
10097            )
10098        })?;
10099        offer.status = "cancelled".to_string();
10100        offer.last_update_time = now;
10101        if let Some(cr) = self.capacity_reservations.get_mut(capacity_reservation_id) {
10102            cr.pending_billing_owner_account_id = None;
10103        }
10104        Ok(self.billing_ownership_offers.get(&key).unwrap())
10105    }
10106
10107    /// Reject a pending billing-ownership offer (recipient side).
10108    pub fn reject_capacity_reservation_billing_ownership(
10109        &mut self,
10110        capacity_reservation_id: &str,
10111        rejecter_account_id: &str,
10112    ) -> Result<&BillingOwnershipOffer, Ec2Error> {
10113        let key = (
10114            capacity_reservation_id.to_string(),
10115            rejecter_account_id.to_string(),
10116        );
10117        let now = self.now_iso();
10118        let offer = self.billing_ownership_offers.get_mut(&key).ok_or_else(|| {
10119            Ec2Error::InvalidBillingOwnershipOfferNotFound(
10120                capacity_reservation_id.to_string(),
10121                rejecter_account_id.to_string(),
10122            )
10123        })?;
10124        offer.status = "rejected".to_string();
10125        offer.last_update_time = now;
10126        if let Some(cr) = self.capacity_reservations.get_mut(capacity_reservation_id) {
10127            cr.pending_billing_owner_account_id = None;
10128        }
10129        Ok(self.billing_ownership_offers.get(&key).unwrap())
10130    }
10131
10132    /// Create a Capacity Manager data export.
10133    #[allow(clippy::too_many_arguments)]
10134    pub fn create_capacity_manager_data_export(
10135        &mut self,
10136        schedule: String,
10137        organization_account_ids: Vec<String>,
10138        output_format: String,
10139        s3_destination: S3DestinationOptions,
10140        tags: Tags,
10141    ) -> Result<&CapacityManagerDataExport, Ec2Error> {
10142        if !matches!(
10143            schedule.as_str(),
10144            "ondemand" | "daily" | "weekly" | "monthly" | "hourly"
10145        ) {
10146            return Err(Ec2Error::InvalidParameterValue(format!(
10147                "Invalid Schedule: {schedule}"
10148            )));
10149        }
10150        if !matches!(output_format.as_str(), "parquet" | "csv") {
10151            return Err(Ec2Error::InvalidParameterValue(format!(
10152                "Invalid OutputFormat: {output_format}"
10153            )));
10154        }
10155        let id = self.next_capacity_manager_data_export_id();
10156        let now = self.now_iso();
10157        let export = CapacityManagerDataExport {
10158            data_export_id: id.clone(),
10159            schedule,
10160            organization_account_ids,
10161            output_format,
10162            s3_destination,
10163            status: "active".to_string(),
10164            status_message: None,
10165            last_export_time: None,
10166            next_export_time: None,
10167            create_time: now,
10168            tags,
10169        };
10170        self.capacity_manager_data_exports
10171            .insert(id.clone(), export);
10172        Ok(self.capacity_manager_data_exports.get(&id).unwrap())
10173    }
10174
10175    /// Delete a Capacity Manager data export by id.
10176    pub fn delete_capacity_manager_data_export(
10177        &mut self,
10178        data_export_id: &str,
10179    ) -> Result<CapacityManagerDataExport, Ec2Error> {
10180        self.capacity_manager_data_exports
10181            .remove(data_export_id)
10182            .ok_or_else(|| {
10183                Ec2Error::InvalidCapacityManagerDataExportNotFound(data_export_id.to_string())
10184            })
10185    }
10186
10187    /// Update account-level Capacity Manager Organizations access.
10188    pub fn update_capacity_manager_organizations_access(
10189        &mut self,
10190        organizations_access: bool,
10191    ) -> &CapacityManagerOrganizationsAccess {
10192        let now = self.now_iso();
10193        let new_state = if organizations_access {
10194            "enabled"
10195        } else {
10196            "disabled"
10197        };
10198        self.capacity_manager_organizations_access = Some(CapacityManagerOrganizationsAccess {
10199            state: new_state.to_string(),
10200            last_updated_time: now,
10201        });
10202        self.capacity_manager_organizations_access.as_ref().unwrap()
10203    }
10204
10205    /// Split an existing capacity reservation into two (the source loses
10206    /// `instance_count` instances, the new one gets them).
10207    #[allow(clippy::too_many_arguments)]
10208    pub fn create_capacity_reservation_by_splitting(
10209        &mut self,
10210        source_capacity_reservation_id: &str,
10211        instance_count: i32,
10212        account_id: &str,
10213        region: &str,
10214        tags: Tags,
10215    ) -> Result<(CapacityReservation, CapacityReservation), Ec2Error> {
10216        if instance_count <= 0 {
10217            return Err(Ec2Error::InvalidParameterValue(
10218                "InstanceCount must be > 0".to_string(),
10219            ));
10220        }
10221        let source = self
10222            .capacity_reservations
10223            .get(source_capacity_reservation_id)
10224            .ok_or_else(|| {
10225                Ec2Error::CapacityReservationNotFound(source_capacity_reservation_id.to_string())
10226            })?
10227            .clone();
10228        if source.available_instance_count < instance_count {
10229            return Err(Ec2Error::InsufficientCapacityReservationCapacity(
10230                source_capacity_reservation_id.to_string(),
10231                source.available_instance_count,
10232                instance_count,
10233            ));
10234        }
10235        // Decrement the source.
10236        let src_mut = self
10237            .capacity_reservations
10238            .get_mut(source_capacity_reservation_id)
10239            .unwrap();
10240        src_mut.total_instance_count -= instance_count;
10241        src_mut.available_instance_count -= instance_count;
10242        let updated_source = src_mut.clone();
10243        // Create the new split reservation, inheriting most properties of
10244        // the source.
10245        let new_id = self.next_capacity_reservation_id();
10246        let arn = format!("arn:aws:ec2:{region}:{account_id}:capacity-reservation/{new_id}");
10247        let new_cr = CapacityReservation {
10248            capacity_reservation_id: new_id.clone(),
10249            capacity_reservation_arn: arn,
10250            owner_id: account_id.to_string(),
10251            instance_type: source.instance_type.clone(),
10252            instance_platform: source.instance_platform.clone(),
10253            availability_zone: source.availability_zone.clone(),
10254            tenancy: source.tenancy.clone(),
10255            total_instance_count: instance_count,
10256            available_instance_count: instance_count,
10257            ebs_optimized: source.ebs_optimized,
10258            ephemeral_storage: source.ephemeral_storage,
10259            state: "active".to_string(),
10260            start_date: self.now_iso(),
10261            end_date: source.end_date.clone(),
10262            end_date_type: source.end_date_type.clone(),
10263            instance_match_criteria: source.instance_match_criteria.clone(),
10264            create_date: self.now_iso(),
10265            outpost_arn: source.outpost_arn.clone(),
10266            placement_group_arn: source.placement_group_arn.clone(),
10267            tags,
10268            pending_billing_owner_account_id: None,
10269            billing_owner_account_id: Some(account_id.to_string()),
10270            target_capacity_reservation_id: Some(source_capacity_reservation_id.to_string()),
10271            reservation_type: source.reservation_type.clone(),
10272            commitment_info: None,
10273        };
10274        self.capacity_reservations
10275            .insert(new_id.clone(), new_cr.clone());
10276        Ok((updated_source, new_cr))
10277    }
10278
10279    /// Move some instances from a source capacity reservation to a destination.
10280    pub fn move_capacity_reservation_instances(
10281        &mut self,
10282        source_capacity_reservation_id: &str,
10283        destination_capacity_reservation_id: &str,
10284        instance_count: i32,
10285    ) -> Result<(CapacityReservation, CapacityReservation), Ec2Error> {
10286        if instance_count <= 0 {
10287            return Err(Ec2Error::InvalidParameterValue(
10288                "InstanceCount must be > 0".to_string(),
10289            ));
10290        }
10291        if !self
10292            .capacity_reservations
10293            .contains_key(destination_capacity_reservation_id)
10294        {
10295            return Err(Ec2Error::CapacityReservationNotFound(
10296                destination_capacity_reservation_id.to_string(),
10297            ));
10298        }
10299        let src = self
10300            .capacity_reservations
10301            .get(source_capacity_reservation_id)
10302            .ok_or_else(|| {
10303                Ec2Error::CapacityReservationNotFound(source_capacity_reservation_id.to_string())
10304            })?
10305            .clone();
10306        if src.available_instance_count < instance_count {
10307            return Err(Ec2Error::InsufficientCapacityReservationCapacity(
10308                source_capacity_reservation_id.to_string(),
10309                src.available_instance_count,
10310                instance_count,
10311            ));
10312        }
10313        let src_mut = self
10314            .capacity_reservations
10315            .get_mut(source_capacity_reservation_id)
10316            .unwrap();
10317        src_mut.total_instance_count -= instance_count;
10318        src_mut.available_instance_count -= instance_count;
10319        let new_src = src_mut.clone();
10320        let dest_mut = self
10321            .capacity_reservations
10322            .get_mut(destination_capacity_reservation_id)
10323            .unwrap();
10324        dest_mut.total_instance_count += instance_count;
10325        dest_mut.available_instance_count += instance_count;
10326        let new_dest = dest_mut.clone();
10327        Ok((new_src, new_dest))
10328    }
10329
10330    /// Modify the per-instance capacity-reservation specification.
10331    pub fn modify_instance_capacity_reservation_attributes(
10332        &mut self,
10333        instance_id: &str,
10334        spec: CapacityReservationSpecificationResponse,
10335    ) -> Result<(), Ec2Error> {
10336        let inst = self
10337            .instances
10338            .get_mut(instance_id)
10339            .ok_or_else(|| Ec2Error::InstanceNotFound(instance_id.to_string()))?;
10340        inst.capacity_reservation_specification = Some(spec);
10341        Ok(())
10342    }
10343
10344    /// Create an interruptible capacity-reservation allocation.
10345    #[allow(clippy::too_many_arguments)]
10346    pub fn create_interruptible_capacity_reservation_allocation(
10347        &mut self,
10348        capacity_reservation_id: &str,
10349        instance_count: i32,
10350        start_date_time: String,
10351        end_date_time: String,
10352        allocation_type: String,
10353        tags: Tags,
10354    ) -> Result<&InterruptibleCapacityReservationAllocation, Ec2Error> {
10355        if !self
10356            .capacity_reservations
10357            .contains_key(capacity_reservation_id)
10358        {
10359            return Err(Ec2Error::CapacityReservationNotFound(
10360                capacity_reservation_id.to_string(),
10361            ));
10362        }
10363        if instance_count <= 0 {
10364            return Err(Ec2Error::InvalidParameterValue(
10365                "InstanceCount must be > 0".to_string(),
10366            ));
10367        }
10368        if !matches!(allocation_type.as_str(), "scheduled" | "on-demand") {
10369            return Err(Ec2Error::InvalidParameterValue(format!(
10370                "Invalid AllocationType: {allocation_type}"
10371            )));
10372        }
10373        let id = self.next_interruptible_capacity_reservation_allocation_id();
10374        let alloc = InterruptibleCapacityReservationAllocation {
10375            allocation_id: id.clone(),
10376            capacity_reservation_id: capacity_reservation_id.to_string(),
10377            instance_count,
10378            start_date_time,
10379            end_date_time,
10380            state: "scheduled".to_string(),
10381            allocation_type,
10382            tags,
10383        };
10384        self.interruptible_capacity_reservation_allocations
10385            .insert(id.clone(), alloc);
10386        Ok(self
10387            .interruptible_capacity_reservation_allocations
10388            .get(&id)
10389            .unwrap())
10390    }
10391
10392    /// Update an interruptible capacity-reservation allocation.
10393    pub fn update_interruptible_capacity_reservation_allocation(
10394        &mut self,
10395        allocation_id: &str,
10396        instance_count: Option<i32>,
10397        start_date_time: Option<String>,
10398        end_date_time: Option<String>,
10399        allocation_type: Option<String>,
10400    ) -> Result<&InterruptibleCapacityReservationAllocation, Ec2Error> {
10401        let alloc = self
10402            .interruptible_capacity_reservation_allocations
10403            .get_mut(allocation_id)
10404            .ok_or_else(|| {
10405                Ec2Error::InvalidInterruptibleCapacityReservationAllocationNotFound(
10406                    allocation_id.to_string(),
10407                )
10408            })?;
10409        if let Some(c) = instance_count {
10410            if c <= 0 {
10411                return Err(Ec2Error::InvalidParameterValue(
10412                    "InstanceCount must be > 0".to_string(),
10413                ));
10414            }
10415            alloc.instance_count = c;
10416        }
10417        if let Some(s) = start_date_time {
10418            alloc.start_date_time = s;
10419        }
10420        if let Some(e) = end_date_time {
10421            alloc.end_date_time = e;
10422        }
10423        if let Some(t) = allocation_type {
10424            if !matches!(t.as_str(), "scheduled" | "on-demand") {
10425                return Err(Ec2Error::InvalidParameterValue(format!(
10426                    "Invalid AllocationType: {t}"
10427                )));
10428            }
10429            alloc.allocation_type = t;
10430        }
10431        Ok(self
10432            .interruptible_capacity_reservation_allocations
10433            .get(allocation_id)
10434            .unwrap())
10435    }
10436
10437    /// Purchase a capacity block. This also synthesises a fixed-duration
10438    /// capacity reservation backed by the block.
10439    #[allow(clippy::too_many_arguments)]
10440    pub fn purchase_capacity_block(
10441        &mut self,
10442        capacity_block_offering_id: &str,
10443        instance_platform: &str,
10444        instance_type: &str,
10445        instance_count: i32,
10446        availability_zone: &str,
10447        start_date: String,
10448        end_date: String,
10449        tenancy: String,
10450        upfront_fee: String,
10451        commitment_duration_in_seconds: i64,
10452        account_id: &str,
10453        region: &str,
10454        tags: Tags,
10455    ) -> Result<(CapacityBlock, CapacityReservation), Ec2Error> {
10456        if instance_count <= 0 {
10457            return Err(Ec2Error::InvalidParameterValue(
10458                "InstanceCount must be > 0".to_string(),
10459            ));
10460        }
10461        // Synthesise a backing capacity reservation.
10462        let cr_id = self.next_capacity_reservation_id();
10463        let cr_arn = format!("arn:aws:ec2:{region}:{account_id}:capacity-reservation/{cr_id}");
10464        let cr = CapacityReservation {
10465            capacity_reservation_id: cr_id.clone(),
10466            capacity_reservation_arn: cr_arn.clone(),
10467            owner_id: account_id.to_string(),
10468            instance_type: instance_type.to_string(),
10469            instance_platform: instance_platform.to_string(),
10470            availability_zone: availability_zone.to_string(),
10471            tenancy: tenancy.clone(),
10472            total_instance_count: instance_count,
10473            available_instance_count: instance_count,
10474            ebs_optimized: false,
10475            ephemeral_storage: false,
10476            state: "active".to_string(),
10477            start_date: start_date.clone(),
10478            end_date: Some(end_date.clone()),
10479            end_date_type: "limited".to_string(),
10480            instance_match_criteria: "targeted".to_string(),
10481            create_date: self.now_iso(),
10482            outpost_arn: None,
10483            placement_group_arn: None,
10484            tags: tags.clone(),
10485            pending_billing_owner_account_id: None,
10486            billing_owner_account_id: Some(account_id.to_string()),
10487            target_capacity_reservation_id: None,
10488            reservation_type: Some("capacity-block".to_string()),
10489            commitment_info: Some(CapacityReservationCommitmentInfo {
10490                commitment_end_date: Some(end_date.clone()),
10491                committed_instance_count: Some(instance_count),
10492            }),
10493        };
10494        self.capacity_reservations.insert(cr_id.clone(), cr.clone());
10495
10496        let block_id = self.next_capacity_block_id();
10497        let block = CapacityBlock {
10498            capacity_block_id: block_id.clone(),
10499            capacity_reservation_id: cr_id,
10500            capacity_block_offering_id: capacity_block_offering_id.to_string(),
10501            instance_type: instance_type.to_string(),
10502            instance_count,
10503            availability_zone: availability_zone.to_string(),
10504            start_date,
10505            end_date,
10506            tenancy,
10507            currency_code: "USD".to_string(),
10508            upfront_fee,
10509            commitment_duration_in_seconds,
10510            capacity_reservation_arn: cr_arn,
10511            tags,
10512        };
10513        self.capacity_blocks.insert(block_id, block.clone());
10514        Ok((block, cr))
10515    }
10516
10517    /// Purchase a capacity-block extension for an existing block-backed
10518    /// capacity reservation.
10519    #[allow(clippy::too_many_arguments)]
10520    pub fn purchase_capacity_block_extension(
10521        &mut self,
10522        capacity_block_extension_offering_id: &str,
10523        capacity_reservation_id: &str,
10524        capacity_block_extension_duration_hours: i32,
10525        upfront_fee: String,
10526        account_id: &str,
10527        region: &str,
10528    ) -> Result<CapacityBlockExtension, Ec2Error> {
10529        let cr = self
10530            .capacity_reservations
10531            .get(capacity_reservation_id)
10532            .ok_or_else(|| {
10533                Ec2Error::CapacityReservationNotFound(capacity_reservation_id.to_string())
10534            })?
10535            .clone();
10536        if capacity_block_extension_duration_hours <= 0 {
10537            return Err(Ec2Error::InvalidParameterValue(
10538                "CapacityBlockExtensionDurationHours must be > 0".to_string(),
10539            ));
10540        }
10541        let id = self.next_capacity_block_extension_id();
10542        let now = self.now_iso();
10543        let start_date = cr.end_date.clone().unwrap_or_else(|| now.clone());
10544        // Compute end_date as start_date + duration hours. We attempt to
10545        // parse the start as RFC 3339; on failure we fall back to `now` so
10546        // the response still contains a valid timestamp.
10547        let end_date = match chrono::DateTime::parse_from_rfc3339(&start_date) {
10548            Ok(start) => {
10549                let new_end =
10550                    start + chrono::Duration::hours(capacity_block_extension_duration_hours as i64);
10551                new_end
10552                    .with_timezone(&chrono::Utc)
10553                    .format("%Y-%m-%dT%H:%M:%S.000Z")
10554                    .to_string()
10555            }
10556            Err(_) => self.now_iso(),
10557        };
10558        let arn = format!("arn:aws:ec2:{region}:{account_id}:capacity-block-extension/{id}");
10559        let ext = CapacityBlockExtension {
10560            capacity_block_extension_id: id.clone(),
10561            capacity_reservation_id: capacity_reservation_id.to_string(),
10562            instance_type: cr.instance_type.clone(),
10563            availability_zone: cr.availability_zone.clone(),
10564            instance_count: cr.total_instance_count,
10565            availability_zone_id: None,
10566            start_date: start_date.clone(),
10567            end_date: end_date.clone(),
10568            capacity_block_extension_offering_id: capacity_block_extension_offering_id.to_string(),
10569            capacity_block_extension_status: "payment-succeeded".to_string(),
10570            capacity_block_extension_purchase_date: now.clone(),
10571            capacity_block_extension_duration_hours,
10572            currency_code: "USD".to_string(),
10573            upfront_fee,
10574            capacity_reservation_arn: Some(cr.capacity_reservation_arn.clone()),
10575            capacity_block_extension_arn: Some(arn),
10576        };
10577        self.capacity_block_extensions
10578            .insert(id.clone(), ext.clone());
10579        // Bump the underlying reservation's end_date.
10580        if let Some(cr_mut) = self.capacity_reservations.get_mut(capacity_reservation_id) {
10581            cr_mut.end_date = Some(end_date);
10582        }
10583        Ok(ext)
10584    }
10585
10586    // ----- Group 11: Transit Gateway extensions -----
10587
10588    pub fn create_tgw_multicast_domain(
10589        &mut self,
10590        tgw_id: &str,
10591        igmpv2_support: &str,
10592        static_sources_support: &str,
10593        auto_accept_shared_associations: &str,
10594        owner_id: &str,
10595        region: &str,
10596        tags: Tags,
10597    ) -> Result<&TransitGatewayMulticastDomain, Ec2Error> {
10598        if !self.transit_gateways.contains_key(tgw_id) {
10599            return Err(Ec2Error::TransitGatewayNotFound(tgw_id.to_string()));
10600        }
10601        let domain_id = self.next_tgw_multicast_domain_id();
10602        let arn =
10603            format!("arn:aws:ec2:{region}:{owner_id}:transit-gateway-multicast-domain/{domain_id}");
10604        let domain = TransitGatewayMulticastDomain {
10605            transit_gateway_multicast_domain_id: domain_id.clone(),
10606            transit_gateway_id: tgw_id.to_string(),
10607            transit_gateway_multicast_domain_arn: arn,
10608            owner_id: owner_id.to_string(),
10609            igmpv2_support: igmpv2_support.to_string(),
10610            static_sources_support: static_sources_support.to_string(),
10611            auto_accept_shared_associations: auto_accept_shared_associations.to_string(),
10612            state: "available".to_string(),
10613            creation_time: "2024-01-01T00:00:00Z".to_string(),
10614            tags,
10615        };
10616        self.tgw_multicast_domains.insert(domain_id.clone(), domain);
10617        Ok(self.tgw_multicast_domains.get(&domain_id).unwrap())
10618    }
10619
10620    pub fn delete_tgw_multicast_domain(&mut self, domain_id: &str) -> Result<(), Ec2Error> {
10621        if !self.tgw_multicast_domains.contains_key(domain_id) {
10622            return Err(Ec2Error::InvalidTgwMulticastDomainNotFound(
10623                domain_id.to_string(),
10624            ));
10625        }
10626        let has_assoc = self
10627            .tgw_multicast_domain_associations
10628            .keys()
10629            .any(|(d, _)| d == domain_id);
10630        let has_member = self
10631            .tgw_multicast_group_members
10632            .keys()
10633            .any(|(d, _, _)| d == domain_id);
10634        let has_source = self
10635            .tgw_multicast_group_sources
10636            .keys()
10637            .any(|(d, _, _)| d == domain_id);
10638        if has_assoc || has_member || has_source {
10639            return Err(Ec2Error::TgwMulticastDomainInUse(domain_id.to_string()));
10640        }
10641        if let Some(d) = self.tgw_multicast_domains.get_mut(domain_id) {
10642            d.state = "deleted".to_string();
10643        }
10644        Ok(())
10645    }
10646
10647    pub fn associate_tgw_multicast_domain(
10648        &mut self,
10649        domain_id: &str,
10650        attachment_id: &str,
10651        subnet_ids: Vec<String>,
10652    ) -> Result<&TransitGatewayMulticastDomainAssociation, Ec2Error> {
10653        let auto_accept = self
10654            .tgw_multicast_domains
10655            .get(domain_id)
10656            .ok_or_else(|| Ec2Error::InvalidTgwMulticastDomainNotFound(domain_id.to_string()))?
10657            .auto_accept_shared_associations
10658            .clone();
10659        let resource_id = self
10660            .tgw_vpc_attachments
10661            .get(attachment_id)
10662            .map(|a| a.vpc_id.clone())
10663            .unwrap_or_default();
10664        let subnet_state = if auto_accept == "enable" {
10665            "associated"
10666        } else {
10667            "pendingAcceptance"
10668        };
10669        let subnets: Vec<MulticastSubnetAssociation> = subnet_ids
10670            .into_iter()
10671            .map(|s| MulticastSubnetAssociation {
10672                subnet_id: s,
10673                state: subnet_state.to_string(),
10674            })
10675            .collect();
10676        let key = (domain_id.to_string(), attachment_id.to_string());
10677        let assoc = TransitGatewayMulticastDomainAssociation {
10678            transit_gateway_multicast_domain_id: domain_id.to_string(),
10679            transit_gateway_attachment_id: attachment_id.to_string(),
10680            resource_id,
10681            resource_type: "vpc".to_string(),
10682            subnets,
10683        };
10684        self.tgw_multicast_domain_associations
10685            .insert(key.clone(), assoc);
10686        Ok(self.tgw_multicast_domain_associations.get(&key).unwrap())
10687    }
10688
10689    pub fn accept_tgw_multicast_domain_associations(
10690        &mut self,
10691        domain_id: &str,
10692        attachment_id: &str,
10693        subnet_ids: &[String],
10694    ) -> Result<&TransitGatewayMulticastDomainAssociation, Ec2Error> {
10695        let key = (domain_id.to_string(), attachment_id.to_string());
10696        let assoc = self
10697            .tgw_multicast_domain_associations
10698            .get_mut(&key)
10699            .ok_or_else(|| {
10700                Ec2Error::InvalidTgwMulticastDomainAssociationNotFound(
10701                    domain_id.to_string(),
10702                    attachment_id.to_string(),
10703                )
10704            })?;
10705        for s in &mut assoc.subnets {
10706            if subnet_ids.is_empty() || subnet_ids.contains(&s.subnet_id) {
10707                s.state = "associated".to_string();
10708            }
10709        }
10710        Ok(self.tgw_multicast_domain_associations.get(&key).unwrap())
10711    }
10712
10713    pub fn reject_tgw_multicast_domain_associations(
10714        &mut self,
10715        domain_id: &str,
10716        attachment_id: &str,
10717        subnet_ids: &[String],
10718    ) -> Result<&TransitGatewayMulticastDomainAssociation, Ec2Error> {
10719        let key = (domain_id.to_string(), attachment_id.to_string());
10720        let assoc = self
10721            .tgw_multicast_domain_associations
10722            .get_mut(&key)
10723            .ok_or_else(|| {
10724                Ec2Error::InvalidTgwMulticastDomainAssociationNotFound(
10725                    domain_id.to_string(),
10726                    attachment_id.to_string(),
10727                )
10728            })?;
10729        for s in &mut assoc.subnets {
10730            if subnet_ids.is_empty() || subnet_ids.contains(&s.subnet_id) {
10731                s.state = "rejected".to_string();
10732            }
10733        }
10734        Ok(self.tgw_multicast_domain_associations.get(&key).unwrap())
10735    }
10736
10737    pub fn disassociate_tgw_multicast_domain(
10738        &mut self,
10739        domain_id: &str,
10740        attachment_id: &str,
10741    ) -> Result<TransitGatewayMulticastDomainAssociation, Ec2Error> {
10742        let key = (domain_id.to_string(), attachment_id.to_string());
10743        let mut assoc = self
10744            .tgw_multicast_domain_associations
10745            .remove(&key)
10746            .ok_or_else(|| {
10747                Ec2Error::InvalidTgwMulticastDomainAssociationNotFound(
10748                    domain_id.to_string(),
10749                    attachment_id.to_string(),
10750                )
10751            })?;
10752        for s in &mut assoc.subnets {
10753            s.state = "disassociated".to_string();
10754        }
10755        Ok(assoc)
10756    }
10757
10758    pub fn register_tgw_multicast_group_members(
10759        &mut self,
10760        domain_id: &str,
10761        group_ip: &str,
10762        nic_ids: Vec<String>,
10763    ) -> Result<Vec<String>, Ec2Error> {
10764        if !self.tgw_multicast_domains.contains_key(domain_id) {
10765            return Err(Ec2Error::InvalidTgwMulticastDomainNotFound(
10766                domain_id.to_string(),
10767            ));
10768        }
10769        let registered = nic_ids.clone();
10770        for nic in nic_ids {
10771            let key = (domain_id.to_string(), group_ip.to_string(), nic.clone());
10772            self.tgw_multicast_group_members.insert(
10773                key,
10774                TransitGatewayMulticastGroupMember {
10775                    transit_gateway_multicast_domain_id: domain_id.to_string(),
10776                    group_ip_address: group_ip.to_string(),
10777                    transit_gateway_attachment_id: None,
10778                    subnet_id: None,
10779                    resource_id: None,
10780                    resource_type: "vpc".to_string(),
10781                    network_interface_id: nic,
10782                    member_type: "igmp".to_string(),
10783                    source_type: "igmp".to_string(),
10784                },
10785            );
10786        }
10787        Ok(registered)
10788    }
10789
10790    pub fn deregister_tgw_multicast_group_members(
10791        &mut self,
10792        domain_id: &str,
10793        group_ip: &str,
10794        nic_ids: Vec<String>,
10795    ) -> Result<Vec<String>, Ec2Error> {
10796        if !self.tgw_multicast_domains.contains_key(domain_id) {
10797            return Err(Ec2Error::InvalidTgwMulticastDomainNotFound(
10798                domain_id.to_string(),
10799            ));
10800        }
10801        let mut removed = Vec::new();
10802        for nic in nic_ids {
10803            let key = (domain_id.to_string(), group_ip.to_string(), nic.clone());
10804            if self.tgw_multicast_group_members.remove(&key).is_some() {
10805                removed.push(nic);
10806            }
10807        }
10808        Ok(removed)
10809    }
10810
10811    pub fn register_tgw_multicast_group_sources(
10812        &mut self,
10813        domain_id: &str,
10814        group_ip: &str,
10815        nic_ids: Vec<String>,
10816    ) -> Result<Vec<String>, Ec2Error> {
10817        if !self.tgw_multicast_domains.contains_key(domain_id) {
10818            return Err(Ec2Error::InvalidTgwMulticastDomainNotFound(
10819                domain_id.to_string(),
10820            ));
10821        }
10822        let registered = nic_ids.clone();
10823        for nic in nic_ids {
10824            let key = (domain_id.to_string(), group_ip.to_string(), nic.clone());
10825            self.tgw_multicast_group_sources.insert(
10826                key,
10827                TransitGatewayMulticastGroupSource {
10828                    transit_gateway_multicast_domain_id: domain_id.to_string(),
10829                    group_ip_address: group_ip.to_string(),
10830                    transit_gateway_attachment_id: None,
10831                    subnet_id: None,
10832                    resource_id: None,
10833                    resource_type: "vpc".to_string(),
10834                    network_interface_id: nic,
10835                    member_type: "static".to_string(),
10836                    source_type: "static".to_string(),
10837                },
10838            );
10839        }
10840        Ok(registered)
10841    }
10842
10843    pub fn deregister_tgw_multicast_group_sources(
10844        &mut self,
10845        domain_id: &str,
10846        group_ip: &str,
10847        nic_ids: Vec<String>,
10848    ) -> Result<Vec<String>, Ec2Error> {
10849        if !self.tgw_multicast_domains.contains_key(domain_id) {
10850            return Err(Ec2Error::InvalidTgwMulticastDomainNotFound(
10851                domain_id.to_string(),
10852            ));
10853        }
10854        let mut removed = Vec::new();
10855        for nic in nic_ids {
10856            let key = (domain_id.to_string(), group_ip.to_string(), nic.clone());
10857            if self.tgw_multicast_group_sources.remove(&key).is_some() {
10858                removed.push(nic);
10859            }
10860        }
10861        Ok(removed)
10862    }
10863
10864    pub fn create_tgw_connect(
10865        &mut self,
10866        transport_attachment_id: &str,
10867        protocol: &str,
10868        tags: Tags,
10869    ) -> Result<&TransitGatewayConnect, Ec2Error> {
10870        let tgw_id = self
10871            .tgw_vpc_attachments
10872            .get(transport_attachment_id)
10873            .map(|a| a.transit_gateway_id.clone())
10874            .ok_or_else(|| {
10875                Ec2Error::TgwVpcAttachmentNotFound(transport_attachment_id.to_string())
10876            })?;
10877        let attach_id = self.next_tgw_connect_id();
10878        let connect = TransitGatewayConnect {
10879            transit_gateway_attachment_id: attach_id.clone(),
10880            transport_transit_gateway_attachment_id: transport_attachment_id.to_string(),
10881            transit_gateway_id: tgw_id,
10882            state: "available".to_string(),
10883            creation_time: "2024-01-01T00:00:00Z".to_string(),
10884            protocol: protocol.to_string(),
10885            tags,
10886        };
10887        self.tgw_connects.insert(attach_id.clone(), connect);
10888        Ok(self.tgw_connects.get(&attach_id).unwrap())
10889    }
10890
10891    pub fn delete_tgw_connect(&mut self, attach_id: &str) -> Result<(), Ec2Error> {
10892        if !self.tgw_connects.contains_key(attach_id) {
10893            return Err(Ec2Error::InvalidTgwConnectNotFound(attach_id.to_string()));
10894        }
10895        let has_peers = self
10896            .tgw_connect_peers
10897            .values()
10898            .any(|p| p.transit_gateway_attachment_id == attach_id);
10899        if has_peers {
10900            return Err(Ec2Error::TgwConnectInUse(attach_id.to_string()));
10901        }
10902        if let Some(c) = self.tgw_connects.get_mut(attach_id) {
10903            c.state = "deleted".to_string();
10904        }
10905        Ok(())
10906    }
10907
10908    #[allow(clippy::too_many_arguments)]
10909    pub fn create_tgw_connect_peer(
10910        &mut self,
10911        attachment_id: &str,
10912        peer_address: &str,
10913        transit_gateway_address: Option<&str>,
10914        inside_cidr_blocks: Vec<String>,
10915        peer_asn: i64,
10916        tags: Tags,
10917    ) -> Result<&TransitGatewayConnectPeer, Ec2Error> {
10918        if !self.tgw_connects.contains_key(attachment_id) {
10919            return Err(Ec2Error::InvalidTgwConnectNotFound(
10920                attachment_id.to_string(),
10921            ));
10922        }
10923        let peer_id = self.next_tgw_connect_peer_id();
10924        let tgw_addr = transit_gateway_address.unwrap_or("169.254.6.1").to_string();
10925        let bgp = TransitGatewayAttachmentBgpConfiguration {
10926            transit_gateway_asn: 64512,
10927            peer_asn,
10928            transit_gateway_address: tgw_addr.clone(),
10929            peer_address: peer_address.to_string(),
10930            bgp_status: "up".to_string(),
10931        };
10932        let peer = TransitGatewayConnectPeer {
10933            transit_gateway_attachment_id: attachment_id.to_string(),
10934            transit_gateway_connect_peer_id: peer_id.clone(),
10935            state: "available".to_string(),
10936            creation_time: "2024-01-01T00:00:00Z".to_string(),
10937            transit_gateway_address: tgw_addr,
10938            peer_address: peer_address.to_string(),
10939            inside_cidr_blocks,
10940            protocol: "gre".to_string(),
10941            bgp_configurations: vec![bgp],
10942            tags,
10943        };
10944        self.tgw_connect_peers.insert(peer_id.clone(), peer);
10945        Ok(self.tgw_connect_peers.get(&peer_id).unwrap())
10946    }
10947
10948    pub fn delete_tgw_connect_peer(&mut self, peer_id: &str) -> Result<(), Ec2Error> {
10949        // Mark deleted then drop so subsequent `delete_tgw_connect` does not see
10950        // it as a dependent peer.
10951        if !self.tgw_connect_peers.contains_key(peer_id) {
10952            return Err(Ec2Error::InvalidTgwConnectPeerNotFound(peer_id.to_string()));
10953        }
10954        self.tgw_connect_peers.remove(peer_id);
10955        Ok(())
10956    }
10957
10958    pub fn create_tgw_metering_policy(
10959        &mut self,
10960        tgw_id: &str,
10961        name: &str,
10962        description: Option<String>,
10963        owner_id: &str,
10964        region: &str,
10965        tags: Tags,
10966    ) -> Result<&TransitGatewayMeteringPolicy, Ec2Error> {
10967        if !self.transit_gateways.contains_key(tgw_id) {
10968            return Err(Ec2Error::TransitGatewayNotFound(tgw_id.to_string()));
10969        }
10970        let policy_id = self.next_tgw_metering_policy_id();
10971        let arn =
10972            format!("arn:aws:ec2:{region}:{owner_id}:transit-gateway-metering-policy/{policy_id}");
10973        let policy = TransitGatewayMeteringPolicy {
10974            transit_gateway_metering_policy_id: policy_id.clone(),
10975            transit_gateway_metering_policy_arn: arn,
10976            transit_gateway_id: tgw_id.to_string(),
10977            name: name.to_string(),
10978            description,
10979            state: "available".to_string(),
10980            tags,
10981            last_updated_time: "2024-01-01T00:00:00Z".to_string(),
10982            version: 1,
10983            middlebox_attachment_ids: Vec::new(),
10984        };
10985        self.tgw_metering_policies.insert(policy_id.clone(), policy);
10986        Ok(self.tgw_metering_policies.get(&policy_id).unwrap())
10987    }
10988
10989    pub fn delete_tgw_metering_policy(&mut self, policy_id: &str) -> Result<(), Ec2Error> {
10990        if !self.tgw_metering_policies.contains_key(policy_id) {
10991            return Err(Ec2Error::InvalidTgwMeteringPolicyNotFound(
10992                policy_id.to_string(),
10993            ));
10994        }
10995        // Cascade entries.
10996        self.tgw_metering_policy_entries
10997            .retain(|(p, _), _| p != policy_id);
10998        if let Some(p) = self.tgw_metering_policies.get_mut(policy_id) {
10999            p.state = "deleted".to_string();
11000        }
11001        Ok(())
11002    }
11003
11004    pub fn modify_tgw_metering_policy(
11005        &mut self,
11006        policy_id: &str,
11007        new_name: Option<String>,
11008        new_description: Option<String>,
11009    ) -> Result<&TransitGatewayMeteringPolicy, Ec2Error> {
11010        let p = self
11011            .tgw_metering_policies
11012            .get_mut(policy_id)
11013            .ok_or_else(|| Ec2Error::InvalidTgwMeteringPolicyNotFound(policy_id.to_string()))?;
11014        if let Some(n) = new_name {
11015            p.name = n;
11016        }
11017        if let Some(d) = new_description {
11018            p.description = Some(d);
11019        }
11020        p.version += 1;
11021        p.last_updated_time = "2024-01-01T00:00:01Z".to_string();
11022        Ok(self.tgw_metering_policies.get(policy_id).unwrap())
11023    }
11024
11025    #[allow(clippy::too_many_arguments)]
11026    pub fn create_tgw_metering_policy_entry(
11027        &mut self,
11028        policy_id: &str,
11029        sequence_number: i32,
11030        action: &str,
11031        source_cidr_block: Option<String>,
11032        destination_cidr_block: Option<String>,
11033        protocol: Option<String>,
11034        source_port: Option<String>,
11035        destination_port: Option<String>,
11036        dimensions: Vec<String>,
11037    ) -> Result<&TransitGatewayMeteringPolicyEntry, Ec2Error> {
11038        if !self.tgw_metering_policies.contains_key(policy_id) {
11039            return Err(Ec2Error::InvalidTgwMeteringPolicyNotFound(
11040                policy_id.to_string(),
11041            ));
11042        }
11043        let entry_id = self.next_tgw_metering_policy_entry_id();
11044        let entry = TransitGatewayMeteringPolicyEntry {
11045            transit_gateway_metering_policy_entry_id: entry_id.clone(),
11046            transit_gateway_metering_policy_id: policy_id.to_string(),
11047            sequence_number,
11048            action: action.to_string(),
11049            source_cidr_block,
11050            destination_cidr_block,
11051            protocol,
11052            source_port,
11053            destination_port,
11054            dimensions,
11055            state: "available".to_string(),
11056        };
11057        let key = (policy_id.to_string(), entry_id.clone());
11058        self.tgw_metering_policy_entries.insert(key.clone(), entry);
11059        Ok(self.tgw_metering_policy_entries.get(&key).unwrap())
11060    }
11061
11062    pub fn delete_tgw_metering_policy_entry(
11063        &mut self,
11064        policy_id: &str,
11065        entry_id: &str,
11066    ) -> Result<(), Ec2Error> {
11067        let key = (policy_id.to_string(), entry_id.to_string());
11068        if self.tgw_metering_policy_entries.remove(&key).is_none() {
11069            return Err(Ec2Error::InvalidTgwMeteringPolicyEntryNotFound(
11070                entry_id.to_string(),
11071            ));
11072        }
11073        Ok(())
11074    }
11075
11076    pub fn create_tgw_policy_table(
11077        &mut self,
11078        tgw_id: &str,
11079        tags: Tags,
11080    ) -> Result<&TransitGatewayPolicyTable, Ec2Error> {
11081        if !self.transit_gateways.contains_key(tgw_id) {
11082            return Err(Ec2Error::TransitGatewayNotFound(tgw_id.to_string()));
11083        }
11084        let id = self.next_tgw_policy_table_id();
11085        let pt = TransitGatewayPolicyTable {
11086            transit_gateway_policy_table_id: id.clone(),
11087            transit_gateway_id: tgw_id.to_string(),
11088            state: "available".to_string(),
11089            creation_time: "2024-01-01T00:00:00Z".to_string(),
11090            tags,
11091        };
11092        self.tgw_policy_tables.insert(id.clone(), pt);
11093        Ok(self.tgw_policy_tables.get(&id).unwrap())
11094    }
11095
11096    pub fn delete_tgw_policy_table(&mut self, id: &str) -> Result<(), Ec2Error> {
11097        let pt = self
11098            .tgw_policy_tables
11099            .get_mut(id)
11100            .ok_or_else(|| Ec2Error::InvalidTgwPolicyTableNotFound(id.to_string()))?;
11101        pt.state = "deleted".to_string();
11102        // Drop associations cascading.
11103        self.tgw_policy_table_associations
11104            .retain(|(p, _), _| p != id);
11105        Ok(())
11106    }
11107
11108    pub fn associate_tgw_policy_table(
11109        &mut self,
11110        policy_table_id: &str,
11111        attachment_id: &str,
11112    ) -> Result<&TransitGatewayPolicyTableAssociation, Ec2Error> {
11113        if !self.tgw_policy_tables.contains_key(policy_table_id) {
11114            return Err(Ec2Error::InvalidTgwPolicyTableNotFound(
11115                policy_table_id.to_string(),
11116            ));
11117        }
11118        let resource_id = self
11119            .tgw_vpc_attachments
11120            .get(attachment_id)
11121            .map(|a| a.vpc_id.clone())
11122            .unwrap_or_default();
11123        let key = (policy_table_id.to_string(), attachment_id.to_string());
11124        let assoc = TransitGatewayPolicyTableAssociation {
11125            transit_gateway_policy_table_id: policy_table_id.to_string(),
11126            transit_gateway_attachment_id: attachment_id.to_string(),
11127            resource_type: "vpc".to_string(),
11128            resource_id,
11129            state: "associated".to_string(),
11130        };
11131        self.tgw_policy_table_associations
11132            .insert(key.clone(), assoc);
11133        Ok(self.tgw_policy_table_associations.get(&key).unwrap())
11134    }
11135
11136    pub fn disassociate_tgw_policy_table(
11137        &mut self,
11138        policy_table_id: &str,
11139        attachment_id: &str,
11140    ) -> Result<TransitGatewayPolicyTableAssociation, Ec2Error> {
11141        let key = (policy_table_id.to_string(), attachment_id.to_string());
11142        let mut assoc = self
11143            .tgw_policy_table_associations
11144            .remove(&key)
11145            .ok_or_else(|| {
11146                Ec2Error::InvalidTgwPolicyTableAssociationNotFound(
11147                    policy_table_id.to_string(),
11148                    attachment_id.to_string(),
11149                )
11150            })?;
11151        assoc.state = "disassociated".to_string();
11152        Ok(assoc)
11153    }
11154
11155    pub fn create_tgw_prefix_list_reference(
11156        &mut self,
11157        route_table_id: &str,
11158        prefix_list_id: &str,
11159        owner_id: &str,
11160        blackhole: bool,
11161        attachment_id: Option<String>,
11162    ) -> Result<&TransitGatewayPrefixListReference, Ec2Error> {
11163        if !self.tgw_route_tables.contains_key(route_table_id) {
11164            return Err(Ec2Error::TgwRouteTableNotFound(route_table_id.to_string()));
11165        }
11166        let (resource_id, resource_type) = if let Some(att_id) = &attachment_id {
11167            let r = self
11168                .tgw_vpc_attachments
11169                .get(att_id)
11170                .map(|a| (a.vpc_id.clone(), "vpc".to_string()));
11171            r.unwrap_or((String::new(), String::new()))
11172        } else {
11173            (String::new(), String::new())
11174        };
11175        let key = (route_table_id.to_string(), prefix_list_id.to_string());
11176        let r = TransitGatewayPrefixListReference {
11177            transit_gateway_route_table_id: route_table_id.to_string(),
11178            prefix_list_id: prefix_list_id.to_string(),
11179            prefix_list_owner_id: owner_id.to_string(),
11180            state: "available".to_string(),
11181            blackhole,
11182            transit_gateway_attachment_id: attachment_id,
11183            resource_id: if resource_id.is_empty() {
11184                None
11185            } else {
11186                Some(resource_id)
11187            },
11188            resource_type: if resource_type.is_empty() {
11189                None
11190            } else {
11191                Some(resource_type)
11192            },
11193        };
11194        self.tgw_prefix_list_references.insert(key.clone(), r);
11195        Ok(self.tgw_prefix_list_references.get(&key).unwrap())
11196    }
11197
11198    pub fn delete_tgw_prefix_list_reference(
11199        &mut self,
11200        route_table_id: &str,
11201        prefix_list_id: &str,
11202    ) -> Result<TransitGatewayPrefixListReference, Ec2Error> {
11203        let key = (route_table_id.to_string(), prefix_list_id.to_string());
11204        let mut r = self
11205            .tgw_prefix_list_references
11206            .remove(&key)
11207            .ok_or_else(|| {
11208                Ec2Error::InvalidTgwPrefixListReferenceNotFound(
11209                    route_table_id.to_string(),
11210                    prefix_list_id.to_string(),
11211                )
11212            })?;
11213        r.state = "deleted".to_string();
11214        Ok(r)
11215    }
11216
11217    pub fn modify_tgw_prefix_list_reference(
11218        &mut self,
11219        route_table_id: &str,
11220        prefix_list_id: &str,
11221        blackhole: Option<bool>,
11222        attachment_id: Option<String>,
11223    ) -> Result<&TransitGatewayPrefixListReference, Ec2Error> {
11224        let key = (route_table_id.to_string(), prefix_list_id.to_string());
11225        if !self.tgw_prefix_list_references.contains_key(&key) {
11226            return Err(Ec2Error::InvalidTgwPrefixListReferenceNotFound(
11227                route_table_id.to_string(),
11228                prefix_list_id.to_string(),
11229            ));
11230        }
11231        // Look up attachment metadata (immutable borrow) before mutating.
11232        let new_resource = attachment_id.as_ref().and_then(|a| {
11233            self.tgw_vpc_attachments
11234                .get(a)
11235                .map(|att| (att.vpc_id.clone(), "vpc".to_string()))
11236        });
11237        let r = self.tgw_prefix_list_references.get_mut(&key).unwrap();
11238        if let Some(b) = blackhole {
11239            r.blackhole = b;
11240        }
11241        if let Some(a) = attachment_id {
11242            r.transit_gateway_attachment_id = Some(a);
11243            if let Some((rid, rt)) = new_resource {
11244                r.resource_id = Some(rid);
11245                r.resource_type = Some(rt);
11246            }
11247        }
11248        r.state = "available".to_string();
11249        Ok(self.tgw_prefix_list_references.get(&key).unwrap())
11250    }
11251
11252    #[allow(clippy::too_many_arguments)]
11253    pub fn create_tgw_route_table_announcement(
11254        &mut self,
11255        route_table_id: &str,
11256        peering_attachment_id: &str,
11257        announcement_direction: &str,
11258        peer_core_network_id: Option<String>,
11259    ) -> Result<&TransitGatewayRouteTableAnnouncement, Ec2Error> {
11260        let tgw_id = self
11261            .tgw_route_tables
11262            .get(route_table_id)
11263            .map(|r| r.transit_gateway_id.clone())
11264            .ok_or_else(|| Ec2Error::TgwRouteTableNotFound(route_table_id.to_string()))?;
11265        let peer_tgw_id = self
11266            .tgw_peering_attachments
11267            .get(peering_attachment_id)
11268            .map(|p| p.peer_transit_gateway_id.clone())
11269            .ok_or_else(|| {
11270                Ec2Error::TgwPeeringAttachmentNotFound(peering_attachment_id.to_string())
11271            })?;
11272        let id = self.next_tgw_route_table_announcement_id();
11273        let ann = TransitGatewayRouteTableAnnouncement {
11274            transit_gateway_route_table_announcement_id: id.clone(),
11275            transit_gateway_id: tgw_id,
11276            core_network_id: String::new(),
11277            peer_transit_gateway_id: peer_tgw_id,
11278            peer_core_network_id,
11279            peering_attachment_id: peering_attachment_id.to_string(),
11280            announcement_direction: announcement_direction.to_string(),
11281            transit_gateway_route_table_id: route_table_id.to_string(),
11282            state: "available".to_string(),
11283            creation_time: "2024-01-01T00:00:00Z".to_string(),
11284            tags: Tags::new(),
11285        };
11286        self.tgw_route_table_announcements.insert(id.clone(), ann);
11287        Ok(self.tgw_route_table_announcements.get(&id).unwrap())
11288    }
11289
11290    pub fn delete_tgw_route_table_announcement(&mut self, id: &str) -> Result<(), Ec2Error> {
11291        let ann = self
11292            .tgw_route_table_announcements
11293            .get_mut(id)
11294            .ok_or_else(|| Ec2Error::InvalidTgwRouteTableAnnouncementNotFound(id.to_string()))?;
11295        ann.state = "deleted".to_string();
11296        Ok(())
11297    }
11298
11299    pub fn accept_tgw_vpc_attachment(
11300        &mut self,
11301        attach_id: &str,
11302    ) -> Result<&TransitGatewayVpcAttachment, Ec2Error> {
11303        let att = self
11304            .tgw_vpc_attachments
11305            .get_mut(attach_id)
11306            .ok_or_else(|| Ec2Error::TgwVpcAttachmentNotFound(attach_id.to_string()))?;
11307        if att.state != "pendingAcceptance" {
11308            return Err(Ec2Error::TgwAttachmentNotPendingAcceptance(
11309                attach_id.to_string(),
11310            ));
11311        }
11312        att.state = "available".to_string();
11313        Ok(self.tgw_vpc_attachments.get(attach_id).unwrap())
11314    }
11315
11316    pub fn reject_tgw_vpc_attachment(
11317        &mut self,
11318        attach_id: &str,
11319    ) -> Result<&TransitGatewayVpcAttachment, Ec2Error> {
11320        let att = self
11321            .tgw_vpc_attachments
11322            .get_mut(attach_id)
11323            .ok_or_else(|| Ec2Error::TgwVpcAttachmentNotFound(attach_id.to_string()))?;
11324        if att.state != "pendingAcceptance" {
11325            return Err(Ec2Error::TgwAttachmentNotPendingAcceptance(
11326                attach_id.to_string(),
11327            ));
11328        }
11329        att.state = "rejected".to_string();
11330        Ok(self.tgw_vpc_attachments.get(attach_id).unwrap())
11331    }
11332
11333    pub fn replace_tgw_route(
11334        &mut self,
11335        route_table_id: &str,
11336        cidr: &str,
11337        new_attachment_id: Option<String>,
11338        blackhole: bool,
11339    ) -> Result<TransitGatewayRoute, Ec2Error> {
11340        let routes = self
11341            .tgw_routes
11342            .get_mut(route_table_id)
11343            .ok_or_else(|| Ec2Error::TgwRouteTableNotFound(route_table_id.to_string()))?;
11344        let pos = routes
11345            .iter()
11346            .position(|r| r.destination_cidr_block == cidr)
11347            .ok_or_else(|| {
11348                Ec2Error::TgwRouteNotFound(route_table_id.to_string(), cidr.to_string())
11349            })?;
11350        let r = &mut routes[pos];
11351        r.attachment_id = new_attachment_id;
11352        r.state = if blackhole { "blackhole" } else { "active" }.to_string();
11353        Ok(r.clone())
11354    }
11355
11356    // ----- Group 12: IPAM -----
11357
11358    pub fn next_ipam_id(&mut self) -> String {
11359        self.counters.ipam += 1;
11360        format!("ipam-{:08x}", self.counters.ipam)
11361    }
11362
11363    pub fn next_ipam_scope_id(&mut self) -> String {
11364        self.counters.ipam_scope += 1;
11365        format!("ipam-scope-{:08x}", self.counters.ipam_scope)
11366    }
11367
11368    pub fn next_ipam_pool_id(&mut self) -> String {
11369        self.counters.ipam_pool += 1;
11370        format!("ipam-pool-{:08x}", self.counters.ipam_pool)
11371    }
11372
11373    pub fn next_ipam_pool_cidr_id(&mut self) -> String {
11374        self.counters.ipam_pool_cidr += 1;
11375        format!("ipam-pool-cidr-{:08x}", self.counters.ipam_pool_cidr)
11376    }
11377
11378    pub fn next_ipam_pool_allocation_id(&mut self) -> String {
11379        self.counters.ipam_pool_allocation += 1;
11380        format!("ipam-pool-alloc-{:08x}", self.counters.ipam_pool_allocation)
11381    }
11382
11383    pub fn next_ipam_resource_discovery_id(&mut self) -> String {
11384        self.counters.ipam_resource_discovery += 1;
11385        format!(
11386            "ipam-res-disco-{:08x}",
11387            self.counters.ipam_resource_discovery
11388        )
11389    }
11390
11391    pub fn next_ipam_resource_discovery_assoc_id(&mut self) -> String {
11392        self.counters.ipam_resource_discovery_association += 1;
11393        format!(
11394            "ipam-res-disco-assoc-{:08x}",
11395            self.counters.ipam_resource_discovery_association
11396        )
11397    }
11398
11399    pub fn next_ipam_external_resource_verification_token_id(&mut self) -> String {
11400        self.counters.ipam_external_resource_verification_token += 1;
11401        format!(
11402            "ipam-ext-res-ver-token-{:08x}",
11403            self.counters.ipam_external_resource_verification_token
11404        )
11405    }
11406
11407    pub fn next_ipam_policy_id(&mut self) -> String {
11408        self.counters.ipam_policy += 1;
11409        format!("ipam-policy-{:08x}", self.counters.ipam_policy)
11410    }
11411
11412    pub fn next_ipam_prefix_list_resolver_id(&mut self) -> String {
11413        self.counters.ipam_prefix_list_resolver += 1;
11414        format!(
11415            "ipam-pl-resolver-{:08x}",
11416            self.counters.ipam_prefix_list_resolver
11417        )
11418    }
11419
11420    pub fn next_ipam_prefix_list_resolver_target_id(&mut self) -> String {
11421        self.counters.ipam_prefix_list_resolver_target += 1;
11422        format!(
11423            "ipam-pl-resolver-tgt-{:08x}",
11424            self.counters.ipam_prefix_list_resolver_target
11425        )
11426    }
11427
11428    /// Create an IPAM. Auto-creates a default `private` and a default `public` scope.
11429    #[allow(clippy::too_many_arguments)]
11430    pub fn create_ipam(
11431        &mut self,
11432        region: &str,
11433        owner_id: &str,
11434        description: Option<String>,
11435        operating_regions: Vec<IpamOperatingRegion>,
11436        tier: String,
11437        enable_private_gua: bool,
11438        metered_account: String,
11439        tags: Tags,
11440    ) -> &Ipam {
11441        let ipam_id = self.next_ipam_id();
11442        let ipam_arn = format!(
11443            "arn:aws:ec2::{owner_id}:ipam/{ipam_id}",
11444            owner_id = owner_id,
11445            ipam_id = ipam_id,
11446        );
11447        // Auto-create default scopes.
11448        let private_scope_id = self.next_ipam_scope_id();
11449        let public_scope_id = self.next_ipam_scope_id();
11450        let private_scope_arn = format!(
11451            "arn:aws:ec2::{owner_id}:ipam-scope/{id}",
11452            owner_id = owner_id,
11453            id = private_scope_id,
11454        );
11455        let public_scope_arn = format!(
11456            "arn:aws:ec2::{owner_id}:ipam-scope/{id}",
11457            owner_id = owner_id,
11458            id = public_scope_id,
11459        );
11460        for (sid, sarn, stype) in [
11461            (
11462                private_scope_id.clone(),
11463                private_scope_arn.clone(),
11464                "private",
11465            ),
11466            (public_scope_id.clone(), public_scope_arn.clone(), "public"),
11467        ] {
11468            let scope = IpamScope {
11469                ipam_scope_id: sid.clone(),
11470                ipam_scope_arn: sarn,
11471                ipam_arn: ipam_arn.clone(),
11472                ipam_region: region.to_string(),
11473                ipam_scope_type: stype.to_string(),
11474                is_default: true,
11475                description: None,
11476                pool_count: 0,
11477                state: "create-complete".to_string(),
11478                tags: Tags::new(),
11479                owner_id: owner_id.to_string(),
11480            };
11481            self.ipam_scopes.insert(sid, scope);
11482        }
11483        let ipam = Ipam {
11484            ipam_id: ipam_id.clone(),
11485            ipam_arn,
11486            ipam_region: region.to_string(),
11487            public_default_scope_id: public_scope_id,
11488            private_default_scope_id: private_scope_id,
11489            scope_count: 2,
11490            description,
11491            operating_regions,
11492            state: "create-complete".to_string(),
11493            owner_id: owner_id.to_string(),
11494            default_resource_discovery_id: None,
11495            default_resource_discovery_association_id: None,
11496            resource_discovery_association_count: 0,
11497            tier,
11498            enable_private_gua,
11499            metered_account,
11500            tags,
11501        };
11502        self.ipams.insert(ipam_id.clone(), ipam);
11503        self.ipams.get(&ipam_id).unwrap()
11504    }
11505
11506    pub fn delete_ipam(&mut self, ipam_id: &str, cascade: bool) -> Result<Ipam, Ec2Error> {
11507        if !self.ipams.contains_key(ipam_id) {
11508            return Err(Ec2Error::InvalidIpamNotFound(ipam_id.to_string()));
11509        }
11510        // Find scopes belonging to this IPAM.
11511        let scope_ids: Vec<String> = self
11512            .ipam_scopes
11513            .values()
11514            .filter(|s| {
11515                self.ipams
11516                    .get(ipam_id)
11517                    .map(|i| s.ipam_arn == i.ipam_arn)
11518                    .unwrap_or(false)
11519            })
11520            .map(|s| s.ipam_scope_id.clone())
11521            .collect();
11522        // Find pools belonging to those scopes.
11523        let pool_ids: Vec<String> = self
11524            .ipam_pools
11525            .values()
11526            .filter(|p| {
11527                self.ipam_scopes.values().any(|s| {
11528                    scope_ids.contains(&s.ipam_scope_id) && s.ipam_scope_arn == p.ipam_scope_arn
11529                })
11530            })
11531            .map(|p| p.ipam_pool_id.clone())
11532            .collect();
11533        if !pool_ids.is_empty() && !cascade {
11534            return Err(Ec2Error::IpamInUse(ipam_id.to_string()));
11535        }
11536        if cascade {
11537            for pid in &pool_ids {
11538                self.ipam_pools.remove(pid);
11539                self.ipam_pool_cidrs.retain(|(pool, _), _| pool != pid);
11540                self.ipam_pool_allocations
11541                    .retain(|(pool, _), _| pool != pid);
11542            }
11543            for sid in &scope_ids {
11544                self.ipam_scopes.remove(sid);
11545            }
11546        } else {
11547            // Even without cascade we still wipe the auto-created default scopes.
11548            for sid in &scope_ids {
11549                self.ipam_scopes.remove(sid);
11550            }
11551        }
11552        let mut ipam = self.ipams.remove(ipam_id).unwrap();
11553        ipam.state = "delete-complete".to_string();
11554        Ok(ipam)
11555    }
11556
11557    pub fn modify_ipam(
11558        &mut self,
11559        ipam_id: &str,
11560        description: Option<String>,
11561        add_operating_regions: Vec<IpamOperatingRegion>,
11562        remove_operating_regions: Vec<String>,
11563        tier: Option<String>,
11564        enable_private_gua: Option<bool>,
11565        metered_account: Option<String>,
11566    ) -> Result<&Ipam, Ec2Error> {
11567        let ipam = self
11568            .ipams
11569            .get_mut(ipam_id)
11570            .ok_or_else(|| Ec2Error::InvalidIpamNotFound(ipam_id.to_string()))?;
11571        if let Some(d) = description {
11572            ipam.description = Some(d);
11573        }
11574        if !remove_operating_regions.is_empty() {
11575            ipam.operating_regions
11576                .retain(|r| !remove_operating_regions.contains(&r.region_name));
11577        }
11578        for r in add_operating_regions {
11579            if !ipam
11580                .operating_regions
11581                .iter()
11582                .any(|x| x.region_name == r.region_name)
11583            {
11584                ipam.operating_regions.push(r);
11585            }
11586        }
11587        if let Some(t) = tier {
11588            ipam.tier = t;
11589        }
11590        if let Some(v) = enable_private_gua {
11591            ipam.enable_private_gua = v;
11592        }
11593        if let Some(m) = metered_account {
11594            ipam.metered_account = m;
11595        }
11596        ipam.state = "modify-complete".to_string();
11597        Ok(ipam)
11598    }
11599
11600    pub fn create_ipam_scope(
11601        &mut self,
11602        ipam_id: &str,
11603        description: Option<String>,
11604        tags: Tags,
11605    ) -> Result<&IpamScope, Ec2Error> {
11606        let ipam = self
11607            .ipams
11608            .get(ipam_id)
11609            .ok_or_else(|| Ec2Error::InvalidIpamNotFound(ipam_id.to_string()))?;
11610        let owner = ipam.owner_id.clone();
11611        let region = ipam.ipam_region.clone();
11612        let ipam_arn = ipam.ipam_arn.clone();
11613        let scope_id = self.next_ipam_scope_id();
11614        let scope_arn = format!("arn:aws:ec2::{owner}:ipam-scope/{scope_id}",);
11615        let scope = IpamScope {
11616            ipam_scope_id: scope_id.clone(),
11617            ipam_scope_arn: scope_arn,
11618            ipam_arn,
11619            ipam_region: region,
11620            ipam_scope_type: "private".to_string(),
11621            is_default: false,
11622            description,
11623            pool_count: 0,
11624            state: "create-complete".to_string(),
11625            tags,
11626            owner_id: owner,
11627        };
11628        self.ipam_scopes.insert(scope_id.clone(), scope);
11629        // Bump IPAM scope_count.
11630        if let Some(i) = self.ipams.get_mut(ipam_id) {
11631            i.scope_count += 1;
11632        }
11633        Ok(self.ipam_scopes.get(&scope_id).unwrap())
11634    }
11635
11636    pub fn delete_ipam_scope(&mut self, scope_id: &str) -> Result<IpamScope, Ec2Error> {
11637        let scope = self
11638            .ipam_scopes
11639            .get(scope_id)
11640            .ok_or_else(|| Ec2Error::InvalidIpamScopeNotFound(scope_id.to_string()))?;
11641        if scope.is_default {
11642            return Err(Ec2Error::IpamScopeIsDefault(scope_id.to_string()));
11643        }
11644        let scope_arn = scope.ipam_scope_arn.clone();
11645        let ipam_arn = scope.ipam_arn.clone();
11646        let has_pools = self
11647            .ipam_pools
11648            .values()
11649            .any(|p| p.ipam_scope_arn == scope_arn);
11650        if has_pools {
11651            return Err(Ec2Error::IpamScopeInUse(scope_id.to_string()));
11652        }
11653        let mut removed = self.ipam_scopes.remove(scope_id).unwrap();
11654        removed.state = "delete-complete".to_string();
11655        // Decrement IPAM scope_count.
11656        if let Some(ipam) = self.ipams.values_mut().find(|i| i.ipam_arn == ipam_arn) {
11657            if ipam.scope_count > 0 {
11658                ipam.scope_count -= 1;
11659            }
11660        }
11661        Ok(removed)
11662    }
11663
11664    pub fn modify_ipam_scope(
11665        &mut self,
11666        scope_id: &str,
11667        description: Option<String>,
11668    ) -> Result<&IpamScope, Ec2Error> {
11669        let scope = self
11670            .ipam_scopes
11671            .get_mut(scope_id)
11672            .ok_or_else(|| Ec2Error::InvalidIpamScopeNotFound(scope_id.to_string()))?;
11673        if let Some(d) = description {
11674            scope.description = Some(d);
11675        }
11676        scope.state = "modify-complete".to_string();
11677        Ok(scope)
11678    }
11679
11680    #[allow(clippy::too_many_arguments)]
11681    pub fn create_ipam_pool(
11682        &mut self,
11683        scope_id: &str,
11684        address_family: String,
11685        description: Option<String>,
11686        locale: Option<String>,
11687        publicly_advertisable: bool,
11688        auto_import: bool,
11689        allocation_min_netmask_length: Option<i32>,
11690        allocation_max_netmask_length: Option<i32>,
11691        allocation_default_netmask_length: Option<i32>,
11692        allocation_resource_tags: Vec<(String, String)>,
11693        aws_service: Option<String>,
11694        public_ip_source: Option<String>,
11695        source_ipam_pool_id: Option<String>,
11696        tags: Tags,
11697    ) -> Result<&IpamPool, Ec2Error> {
11698        let scope = self
11699            .ipam_scopes
11700            .get(scope_id)
11701            .ok_or_else(|| Ec2Error::InvalidIpamScopeNotFound(scope_id.to_string()))?;
11702        let scope_arn = scope.ipam_scope_arn.clone();
11703        let scope_type = scope.ipam_scope_type.clone();
11704        let ipam_arn = scope.ipam_arn.clone();
11705        let region = scope.ipam_region.clone();
11706        let owner = scope.owner_id.clone();
11707        let pool_id = self.next_ipam_pool_id();
11708        let pool_arn = format!("arn:aws:ec2::{owner}:ipam-pool/{pool_id}",);
11709        let pool = IpamPool {
11710            ipam_pool_id: pool_id.clone(),
11711            source_ipam_pool_id,
11712            ipam_pool_arn: pool_arn,
11713            ipam_scope_arn: scope_arn,
11714            ipam_scope_type: scope_type,
11715            ipam_arn,
11716            ipam_region: region.clone(),
11717            locale: locale.unwrap_or_else(|| region.clone()),
11718            pool_depth: 1,
11719            state: "create-complete".to_string(),
11720            state_message: None,
11721            description,
11722            auto_import,
11723            publicly_advertisable,
11724            address_family,
11725            allocation_min_netmask_length,
11726            allocation_max_netmask_length,
11727            allocation_default_netmask_length,
11728            allocation_resource_tags,
11729            aws_service,
11730            public_ip_source,
11731            source_resource_id: None,
11732            source_resource_type: None,
11733            source_resource_region: None,
11734            source_resource_owner: None,
11735            tags,
11736            owner_id: owner,
11737            allocation_count: 0,
11738        };
11739        self.ipam_pools.insert(pool_id.clone(), pool);
11740        // Bump scope.pool_count.
11741        if let Some(s) = self.ipam_scopes.get_mut(scope_id) {
11742            s.pool_count += 1;
11743        }
11744        Ok(self.ipam_pools.get(&pool_id).unwrap())
11745    }
11746
11747    pub fn delete_ipam_pool(&mut self, pool_id: &str) -> Result<IpamPool, Ec2Error> {
11748        let pool = self
11749            .ipam_pools
11750            .get(pool_id)
11751            .ok_or_else(|| Ec2Error::InvalidIpamPoolNotFound(pool_id.to_string()))?;
11752        let scope_arn = pool.ipam_scope_arn.clone();
11753        let has_allocations = self
11754            .ipam_pool_allocations
11755            .keys()
11756            .any(|(pid, _)| pid == pool_id);
11757        if has_allocations {
11758            return Err(Ec2Error::IpamPoolInUse(pool_id.to_string()));
11759        }
11760        // Remove dependent CIDRs (no allocations means we can clean them up).
11761        self.ipam_pool_cidrs.retain(|(pid, _), _| pid != pool_id);
11762        let mut removed = self.ipam_pools.remove(pool_id).unwrap();
11763        removed.state = "delete-complete".to_string();
11764        // Decrement scope.pool_count.
11765        if let Some(scope) = self
11766            .ipam_scopes
11767            .values_mut()
11768            .find(|s| s.ipam_scope_arn == scope_arn)
11769        {
11770            if scope.pool_count > 0 {
11771                scope.pool_count -= 1;
11772            }
11773        }
11774        Ok(removed)
11775    }
11776
11777    #[allow(clippy::too_many_arguments)]
11778    pub fn modify_ipam_pool(
11779        &mut self,
11780        pool_id: &str,
11781        description: Option<String>,
11782        auto_import: Option<bool>,
11783        allocation_min_netmask_length: Option<i32>,
11784        allocation_max_netmask_length: Option<i32>,
11785        allocation_default_netmask_length: Option<i32>,
11786        add_allocation_resource_tags: Vec<(String, String)>,
11787        remove_allocation_resource_tags: Vec<(String, String)>,
11788    ) -> Result<&IpamPool, Ec2Error> {
11789        let pool = self
11790            .ipam_pools
11791            .get_mut(pool_id)
11792            .ok_or_else(|| Ec2Error::InvalidIpamPoolNotFound(pool_id.to_string()))?;
11793        if let Some(d) = description {
11794            pool.description = Some(d);
11795        }
11796        if let Some(v) = auto_import {
11797            pool.auto_import = v;
11798        }
11799        if let Some(v) = allocation_min_netmask_length {
11800            pool.allocation_min_netmask_length = Some(v);
11801        }
11802        if let Some(v) = allocation_max_netmask_length {
11803            pool.allocation_max_netmask_length = Some(v);
11804        }
11805        if let Some(v) = allocation_default_netmask_length {
11806            pool.allocation_default_netmask_length = Some(v);
11807        }
11808        for tag in add_allocation_resource_tags {
11809            if !pool
11810                .allocation_resource_tags
11811                .iter()
11812                .any(|(k, _)| k == &tag.0)
11813            {
11814                pool.allocation_resource_tags.push(tag);
11815            }
11816        }
11817        for tag in remove_allocation_resource_tags {
11818            pool.allocation_resource_tags.retain(|(k, _)| k != &tag.0);
11819        }
11820        pool.state = "modify-complete".to_string();
11821        Ok(pool)
11822    }
11823
11824    pub fn provision_ipam_pool_cidr(
11825        &mut self,
11826        pool_id: &str,
11827        cidr: Option<String>,
11828        netmask_length: Option<i32>,
11829    ) -> Result<&IpamPoolCidr, Ec2Error> {
11830        if !self.ipam_pools.contains_key(pool_id) {
11831            return Err(Ec2Error::InvalidIpamPoolNotFound(pool_id.to_string()));
11832        }
11833        let cidr_str = cidr.unwrap_or_else(|| {
11834            // Synthesize a placeholder CIDR if only netmask given.
11835            format!(
11836                "10.{}.0.0/{}",
11837                self.counters.ipam_pool_cidr & 0xff,
11838                netmask_length.unwrap_or(16)
11839            )
11840        });
11841        let pool_cidr_id = self.next_ipam_pool_cidr_id();
11842        let entry = IpamPoolCidr {
11843            cidr: cidr_str.clone(),
11844            state: "provisioned".to_string(),
11845            failure_reason: None,
11846            ipam_pool_cidr_id: pool_cidr_id,
11847            netmask_length,
11848        };
11849        let key = (pool_id.to_string(), cidr_str.clone());
11850        self.ipam_pool_cidrs.insert(key.clone(), entry);
11851        Ok(self.ipam_pool_cidrs.get(&key).unwrap())
11852    }
11853
11854    pub fn deprovision_ipam_pool_cidr(
11855        &mut self,
11856        pool_id: &str,
11857        cidr: &str,
11858    ) -> Result<IpamPoolCidr, Ec2Error> {
11859        let key = (pool_id.to_string(), cidr.to_string());
11860        let mut entry = self.ipam_pool_cidrs.remove(&key).ok_or_else(|| {
11861            Ec2Error::InvalidIpamPoolCidrNotFound(pool_id.to_string(), cidr.to_string())
11862        })?;
11863        entry.state = "deprovisioned".to_string();
11864        Ok(entry)
11865    }
11866
11867    pub fn allocate_ipam_pool_cidr(
11868        &mut self,
11869        pool_id: &str,
11870        cidr: Option<String>,
11871        netmask_length: Option<i32>,
11872        description: Option<String>,
11873    ) -> Result<&IpamPoolAllocation, Ec2Error> {
11874        let pool = self
11875            .ipam_pools
11876            .get(pool_id)
11877            .ok_or_else(|| Ec2Error::InvalidIpamPoolNotFound(pool_id.to_string()))?;
11878        let region = pool.ipam_region.clone();
11879        let owner = pool.owner_id.clone();
11880        let allocation_id = self.next_ipam_pool_allocation_id();
11881        let cidr_str = cidr.unwrap_or_else(|| {
11882            format!(
11883                "10.{}.0.0/{}",
11884                self.counters.ipam_pool_allocation & 0xff,
11885                netmask_length.unwrap_or(28)
11886            )
11887        });
11888        let alloc = IpamPoolAllocation {
11889            ipam_pool_allocation_id: allocation_id.clone(),
11890            cidr: cidr_str,
11891            ipam_pool_id: pool_id.to_string(),
11892            description,
11893            resource_id: None,
11894            resource_type: "custom".to_string(),
11895            resource_region: Some(region),
11896            resource_owner: Some(owner),
11897        };
11898        let key = (pool_id.to_string(), allocation_id);
11899        self.ipam_pool_allocations.insert(key.clone(), alloc);
11900        if let Some(p) = self.ipam_pools.get_mut(pool_id) {
11901            p.allocation_count += 1;
11902        }
11903        Ok(self.ipam_pool_allocations.get(&key).unwrap())
11904    }
11905
11906    pub fn release_ipam_pool_allocation(
11907        &mut self,
11908        pool_id: &str,
11909        allocation_id: &str,
11910    ) -> Result<(), Ec2Error> {
11911        let key = (pool_id.to_string(), allocation_id.to_string());
11912        if self.ipam_pool_allocations.remove(&key).is_none() {
11913            return Err(Ec2Error::InvalidIpamPoolAllocationNotFound(
11914                pool_id.to_string(),
11915                allocation_id.to_string(),
11916            ));
11917        }
11918        if let Some(p) = self.ipam_pools.get_mut(pool_id) {
11919            if p.allocation_count > 0 {
11920                p.allocation_count -= 1;
11921            }
11922        }
11923        Ok(())
11924    }
11925
11926    pub fn create_ipam_resource_discovery(
11927        &mut self,
11928        region: &str,
11929        owner_id: &str,
11930        description: Option<String>,
11931        operating_regions: Vec<IpamOperatingRegion>,
11932        tags: Tags,
11933    ) -> &IpamResourceDiscovery {
11934        let id = self.next_ipam_resource_discovery_id();
11935        let arn = format!("arn:aws:ec2::{owner_id}:ipam-resource-discovery/{id}",);
11936        let rd = IpamResourceDiscovery {
11937            ipam_resource_discovery_id: id.clone(),
11938            ipam_resource_discovery_arn: arn,
11939            ipam_resource_discovery_region: region.to_string(),
11940            description,
11941            operating_regions,
11942            is_default: false,
11943            state: "create-complete".to_string(),
11944            owner_id: owner_id.to_string(),
11945            tags,
11946        };
11947        self.ipam_resource_discoveries.insert(id.clone(), rd);
11948        self.ipam_resource_discoveries.get(&id).unwrap()
11949    }
11950
11951    pub fn delete_ipam_resource_discovery(
11952        &mut self,
11953        id: &str,
11954    ) -> Result<IpamResourceDiscovery, Ec2Error> {
11955        let rd = self
11956            .ipam_resource_discoveries
11957            .get(id)
11958            .ok_or_else(|| Ec2Error::InvalidIpamResourceDiscoveryNotFound(id.to_string()))?;
11959        if rd.is_default {
11960            return Err(Ec2Error::IpamResourceDiscoveryInUse(id.to_string()));
11961        }
11962        let has_assoc = self
11963            .ipam_resource_discovery_associations
11964            .values()
11965            .any(|a| a.ipam_resource_discovery_id == id);
11966        if has_assoc {
11967            return Err(Ec2Error::IpamResourceDiscoveryInUse(id.to_string()));
11968        }
11969        let mut removed = self.ipam_resource_discoveries.remove(id).unwrap();
11970        removed.state = "delete-complete".to_string();
11971        Ok(removed)
11972    }
11973
11974    pub fn modify_ipam_resource_discovery(
11975        &mut self,
11976        id: &str,
11977        description: Option<String>,
11978        add_operating_regions: Vec<IpamOperatingRegion>,
11979        remove_operating_regions: Vec<String>,
11980    ) -> Result<&IpamResourceDiscovery, Ec2Error> {
11981        let rd = self
11982            .ipam_resource_discoveries
11983            .get_mut(id)
11984            .ok_or_else(|| Ec2Error::InvalidIpamResourceDiscoveryNotFound(id.to_string()))?;
11985        if let Some(d) = description {
11986            rd.description = Some(d);
11987        }
11988        if !remove_operating_regions.is_empty() {
11989            rd.operating_regions
11990                .retain(|r| !remove_operating_regions.contains(&r.region_name));
11991        }
11992        for r in add_operating_regions {
11993            if !rd
11994                .operating_regions
11995                .iter()
11996                .any(|x| x.region_name == r.region_name)
11997            {
11998                rd.operating_regions.push(r);
11999            }
12000        }
12001        rd.state = "modify-complete".to_string();
12002        Ok(rd)
12003    }
12004
12005    pub fn associate_ipam_resource_discovery(
12006        &mut self,
12007        ipam_id: &str,
12008        resource_discovery_id: &str,
12009        tags: Tags,
12010    ) -> Result<&IpamResourceDiscoveryAssociation, Ec2Error> {
12011        let ipam = self
12012            .ipams
12013            .get(ipam_id)
12014            .ok_or_else(|| Ec2Error::InvalidIpamNotFound(ipam_id.to_string()))?;
12015        if !self
12016            .ipam_resource_discoveries
12017            .contains_key(resource_discovery_id)
12018        {
12019            return Err(Ec2Error::InvalidIpamResourceDiscoveryNotFound(
12020                resource_discovery_id.to_string(),
12021            ));
12022        }
12023        let owner = ipam.owner_id.clone();
12024        let region = ipam.ipam_region.clone();
12025        let ipam_arn = ipam.ipam_arn.clone();
12026        let assoc_id = self.next_ipam_resource_discovery_assoc_id();
12027        let assoc_arn =
12028            format!("arn:aws:ec2::{owner}:ipam-resource-discovery-association/{assoc_id}",);
12029        let assoc = IpamResourceDiscoveryAssociation {
12030            ipam_resource_discovery_association_id: assoc_id.clone(),
12031            ipam_resource_discovery_association_arn: assoc_arn,
12032            ipam_arn,
12033            ipam_id: ipam_id.to_string(),
12034            ipam_region: region,
12035            ipam_resource_discovery_id: resource_discovery_id.to_string(),
12036            owner_id: owner,
12037            is_default: false,
12038            resource_discovery_status: "active".to_string(),
12039            state: "associate-complete".to_string(),
12040            tags,
12041        };
12042        self.ipam_resource_discovery_associations
12043            .insert(assoc_id.clone(), assoc);
12044        if let Some(i) = self.ipams.get_mut(ipam_id) {
12045            i.resource_discovery_association_count += 1;
12046        }
12047        Ok(self
12048            .ipam_resource_discovery_associations
12049            .get(&assoc_id)
12050            .unwrap())
12051    }
12052
12053    pub fn disassociate_ipam_resource_discovery(
12054        &mut self,
12055        assoc_id: &str,
12056    ) -> Result<IpamResourceDiscoveryAssociation, Ec2Error> {
12057        let assoc = self
12058            .ipam_resource_discovery_associations
12059            .get(assoc_id)
12060            .ok_or_else(|| {
12061                Ec2Error::InvalidIpamResourceDiscoveryAssociationNotFound(assoc_id.to_string())
12062            })?;
12063        let ipam_id = assoc.ipam_id.clone();
12064        let mut removed = self
12065            .ipam_resource_discovery_associations
12066            .remove(assoc_id)
12067            .unwrap();
12068        removed.state = "disassociate-complete".to_string();
12069        if let Some(i) = self.ipams.get_mut(&ipam_id) {
12070            if i.resource_discovery_association_count > 0 {
12071                i.resource_discovery_association_count -= 1;
12072            }
12073        }
12074        Ok(removed)
12075    }
12076
12077    pub fn provision_ipam_byoasn(
12078        &mut self,
12079        ipam_id: &str,
12080        asn: &str,
12081        description: Option<String>,
12082    ) -> Result<&IpamByoasn, Ec2Error> {
12083        if !self.ipams.contains_key(ipam_id) {
12084            return Err(Ec2Error::InvalidIpamNotFound(ipam_id.to_string()));
12085        }
12086        let entry = IpamByoasn {
12087            asn: asn.to_string(),
12088            ipam_id: ipam_id.to_string(),
12089            description,
12090            state: "provisioned".to_string(),
12091            status_message: None,
12092        };
12093        let key = (ipam_id.to_string(), asn.to_string());
12094        self.ipam_byoasns.insert(key.clone(), entry);
12095        Ok(self.ipam_byoasns.get(&key).unwrap())
12096    }
12097
12098    pub fn deprovision_ipam_byoasn(
12099        &mut self,
12100        ipam_id: &str,
12101        asn: &str,
12102    ) -> Result<IpamByoasn, Ec2Error> {
12103        let key = (ipam_id.to_string(), asn.to_string());
12104        let mut entry = self.ipam_byoasns.remove(&key).ok_or_else(|| {
12105            Ec2Error::InvalidIpamByoasnNotFound(ipam_id.to_string(), asn.to_string())
12106        })?;
12107        entry.state = "deprovisioned".to_string();
12108        Ok(entry)
12109    }
12110
12111    pub fn associate_ipam_byoasn(
12112        &mut self,
12113        asn: &str,
12114        cidr: &str,
12115    ) -> Result<AsnAssociation, Ec2Error> {
12116        // Find a provisioned BYOASN with the matching ASN.
12117        let exists = self.ipam_byoasns.keys().any(|(_, a)| a == asn);
12118        if !exists {
12119            return Err(Ec2Error::InvalidIpamByoasnNotFound(
12120                String::new(),
12121                asn.to_string(),
12122            ));
12123        }
12124        let cidr_entry = self
12125            .byoip_cidrs
12126            .get_mut(cidr)
12127            .ok_or_else(|| Ec2Error::InvalidByoipCidrNotFound(cidr.to_string()))?;
12128        if cidr_entry.asn_association.is_some() {
12129            return Err(Ec2Error::IpamByoasnAlreadyAssociated(asn.to_string()));
12130        }
12131        let assoc = AsnAssociation {
12132            asn: asn.to_string(),
12133            cidr: cidr.to_string(),
12134            state: "associated".to_string(),
12135            status_message: None,
12136        };
12137        cidr_entry.asn_association = Some(assoc.clone());
12138        Ok(assoc)
12139    }
12140
12141    pub fn disassociate_ipam_byoasn(
12142        &mut self,
12143        asn: &str,
12144        cidr: &str,
12145    ) -> Result<AsnAssociation, Ec2Error> {
12146        let cidr_entry = self
12147            .byoip_cidrs
12148            .get_mut(cidr)
12149            .ok_or_else(|| Ec2Error::InvalidByoipCidrNotFound(cidr.to_string()))?;
12150        let mut assoc = cidr_entry
12151            .asn_association
12152            .take()
12153            .ok_or_else(|| Ec2Error::InvalidIpamByoasnNotFound(String::new(), asn.to_string()))?;
12154        assoc.state = "disassociated".to_string();
12155        Ok(assoc)
12156    }
12157
12158    pub fn create_ipam_external_resource_verification_token(
12159        &mut self,
12160        ipam_id: &str,
12161        tags: Tags,
12162    ) -> Result<&IpamExternalResourceVerificationToken, Ec2Error> {
12163        let ipam = self
12164            .ipams
12165            .get(ipam_id)
12166            .ok_or_else(|| Ec2Error::InvalidIpamNotFound(ipam_id.to_string()))?;
12167        let region = ipam.ipam_region.clone();
12168        let ipam_arn = ipam.ipam_arn.clone();
12169        let owner = ipam.owner_id.clone();
12170        let token_id = self.next_ipam_external_resource_verification_token_id();
12171        let token_arn =
12172            format!("arn:aws:ec2::{owner}:ipam-external-resource-verification-token/{token_id}",);
12173        let now = chrono::Utc::now()
12174            .format("%Y-%m-%dT%H:%M:%S.000Z")
12175            .to_string();
12176        let entry = IpamExternalResourceVerificationToken {
12177            ipam_external_resource_verification_token_id: token_id.clone(),
12178            ipam_external_resource_verification_token_arn: token_arn,
12179            ipam_id: ipam_id.to_string(),
12180            ipam_arn,
12181            ipam_region: region,
12182            token_value: format!("token-{token_id}"),
12183            token_name: format!("token-name-{token_id}"),
12184            not_after: now,
12185            status: "valid".to_string(),
12186            state: "create-complete".to_string(),
12187            tags,
12188        };
12189        self.ipam_external_resource_verification_tokens
12190            .insert(token_id.clone(), entry);
12191        Ok(self
12192            .ipam_external_resource_verification_tokens
12193            .get(&token_id)
12194            .unwrap())
12195    }
12196
12197    pub fn delete_ipam_external_resource_verification_token(
12198        &mut self,
12199        token_id: &str,
12200    ) -> Result<IpamExternalResourceVerificationToken, Ec2Error> {
12201        let mut entry = self
12202            .ipam_external_resource_verification_tokens
12203            .remove(token_id)
12204            .ok_or_else(|| {
12205                Ec2Error::InvalidIpamExternalResourceVerificationTokenNotFound(token_id.to_string())
12206            })?;
12207        entry.state = "delete-complete".to_string();
12208        Ok(entry)
12209    }
12210
12211    pub fn create_ipam_policy(
12212        &mut self,
12213        ipam_id: &str,
12214        policy_name: String,
12215        description: Option<String>,
12216        allocation_rules: Vec<IpamPolicyAllocationRule>,
12217        tags: Tags,
12218    ) -> Result<&IpamPolicy, Ec2Error> {
12219        let ipam = self
12220            .ipams
12221            .get(ipam_id)
12222            .ok_or_else(|| Ec2Error::InvalidIpamNotFound(ipam_id.to_string()))?;
12223        let region = ipam.ipam_region.clone();
12224        let ipam_arn = ipam.ipam_arn.clone();
12225        let owner = ipam.owner_id.clone();
12226        let policy_id = self.next_ipam_policy_id();
12227        let policy_arn = format!("arn:aws:ec2::{owner}:ipam-policy/{policy_id}",);
12228        let policy = IpamPolicy {
12229            ipam_policy_id: policy_id.clone(),
12230            ipam_policy_arn: policy_arn,
12231            ipam_arn,
12232            ipam_region: region,
12233            policy_name,
12234            policy_type: "allocation".to_string(),
12235            description,
12236            state: "create-complete".to_string(),
12237            allocation_rules,
12238            tags,
12239            owner_id: owner,
12240        };
12241        self.ipam_policies.insert(policy_id.clone(), policy);
12242        Ok(self.ipam_policies.get(&policy_id).unwrap())
12243    }
12244
12245    pub fn delete_ipam_policy(&mut self, policy_id: &str) -> Result<IpamPolicy, Ec2Error> {
12246        let mut removed = self
12247            .ipam_policies
12248            .remove(policy_id)
12249            .ok_or_else(|| Ec2Error::InvalidIpamPolicyNotFound(policy_id.to_string()))?;
12250        removed.state = "delete-complete".to_string();
12251        Ok(removed)
12252    }
12253
12254    pub fn modify_ipam_policy_allocation_rules(
12255        &mut self,
12256        policy_id: &str,
12257        add_rules: Vec<IpamPolicyAllocationRule>,
12258        remove_rules: Vec<IpamPolicyAllocationRule>,
12259    ) -> Result<&IpamPolicy, Ec2Error> {
12260        let policy = self
12261            .ipam_policies
12262            .get_mut(policy_id)
12263            .ok_or_else(|| Ec2Error::InvalidIpamPolicyNotFound(policy_id.to_string()))?;
12264        let remove_pool_ids: Vec<Option<String>> = remove_rules
12265            .into_iter()
12266            .map(|r| r.source_ipam_pool_id)
12267            .collect();
12268        if !remove_pool_ids.is_empty() {
12269            policy
12270                .allocation_rules
12271                .retain(|r| !remove_pool_ids.contains(&r.source_ipam_pool_id));
12272        }
12273        for r in add_rules {
12274            if !policy
12275                .allocation_rules
12276                .iter()
12277                .any(|x| x.source_ipam_pool_id == r.source_ipam_pool_id)
12278            {
12279                policy.allocation_rules.push(r);
12280            }
12281        }
12282        Ok(policy)
12283    }
12284
12285    pub fn create_ipam_prefix_list_resolver(
12286        &mut self,
12287        ipam_id: &str,
12288        name: String,
12289        description: Option<String>,
12290        tags: Tags,
12291    ) -> Result<&IpamPrefixListResolver, Ec2Error> {
12292        let ipam = self
12293            .ipams
12294            .get(ipam_id)
12295            .ok_or_else(|| Ec2Error::InvalidIpamNotFound(ipam_id.to_string()))?;
12296        let region = ipam.ipam_region.clone();
12297        let ipam_arn = ipam.ipam_arn.clone();
12298        let owner = ipam.owner_id.clone();
12299        let resolver_id = self.next_ipam_prefix_list_resolver_id();
12300        let resolver_arn = format!("arn:aws:ec2::{owner}:ipam-prefix-list-resolver/{resolver_id}",);
12301        let resolver = IpamPrefixListResolver {
12302            ipam_prefix_list_resolver_id: resolver_id.clone(),
12303            ipam_prefix_list_resolver_arn: resolver_arn,
12304            ipam_arn,
12305            ipam_region: region,
12306            name,
12307            description,
12308            state: "available".to_string(),
12309            owner_id: owner,
12310            target_count: 0,
12311            tags,
12312        };
12313        self.ipam_prefix_list_resolvers
12314            .insert(resolver_id.clone(), resolver);
12315        Ok(self.ipam_prefix_list_resolvers.get(&resolver_id).unwrap())
12316    }
12317
12318    pub fn delete_ipam_prefix_list_resolver(
12319        &mut self,
12320        resolver_id: &str,
12321    ) -> Result<IpamPrefixListResolver, Ec2Error> {
12322        if !self.ipam_prefix_list_resolvers.contains_key(resolver_id) {
12323            return Err(Ec2Error::InvalidIpamPrefixListResolverNotFound(
12324                resolver_id.to_string(),
12325            ));
12326        }
12327        let has_targets = self
12328            .ipam_prefix_list_resolver_targets
12329            .keys()
12330            .any(|(rid, _)| rid == resolver_id);
12331        if has_targets {
12332            return Err(Ec2Error::IpamPrefixListResolverInUse(
12333                resolver_id.to_string(),
12334            ));
12335        }
12336        let mut removed = self.ipam_prefix_list_resolvers.remove(resolver_id).unwrap();
12337        removed.state = "deleted".to_string();
12338        Ok(removed)
12339    }
12340
12341    pub fn modify_ipam_prefix_list_resolver(
12342        &mut self,
12343        resolver_id: &str,
12344        name: Option<String>,
12345        description: Option<String>,
12346    ) -> Result<&IpamPrefixListResolver, Ec2Error> {
12347        let resolver = self
12348            .ipam_prefix_list_resolvers
12349            .get_mut(resolver_id)
12350            .ok_or_else(|| {
12351                Ec2Error::InvalidIpamPrefixListResolverNotFound(resolver_id.to_string())
12352            })?;
12353        if let Some(n) = name {
12354            resolver.name = n;
12355        }
12356        if let Some(d) = description {
12357            resolver.description = Some(d);
12358        }
12359        resolver.state = "modifying".to_string();
12360        Ok(resolver)
12361    }
12362
12363    pub fn create_ipam_prefix_list_resolver_target(
12364        &mut self,
12365        resolver_id: &str,
12366        target_resource_arn: String,
12367        target_resource_type: String,
12368        target_resource_region: String,
12369        tags: Tags,
12370    ) -> Result<&IpamPrefixListResolverTarget, Ec2Error> {
12371        let resolver = self
12372            .ipam_prefix_list_resolvers
12373            .get(resolver_id)
12374            .ok_or_else(|| {
12375                Ec2Error::InvalidIpamPrefixListResolverNotFound(resolver_id.to_string())
12376            })?;
12377        let owner = resolver.owner_id.clone();
12378        let target_id = self.next_ipam_prefix_list_resolver_target_id();
12379        let target = IpamPrefixListResolverTarget {
12380            ipam_prefix_list_resolver_target_id: target_id.clone(),
12381            ipam_prefix_list_resolver_id: resolver_id.to_string(),
12382            target_resource_arn,
12383            target_resource_type,
12384            target_resource_region,
12385            owner_id: owner,
12386            state: "available".to_string(),
12387            tags,
12388        };
12389        let key = (resolver_id.to_string(), target_id);
12390        self.ipam_prefix_list_resolver_targets
12391            .insert(key.clone(), target);
12392        if let Some(r) = self.ipam_prefix_list_resolvers.get_mut(resolver_id) {
12393            r.target_count += 1;
12394        }
12395        Ok(self.ipam_prefix_list_resolver_targets.get(&key).unwrap())
12396    }
12397
12398    pub fn delete_ipam_prefix_list_resolver_target(
12399        &mut self,
12400        resolver_id: &str,
12401        target_id: &str,
12402    ) -> Result<IpamPrefixListResolverTarget, Ec2Error> {
12403        let key = (resolver_id.to_string(), target_id.to_string());
12404        let mut removed = self
12405            .ipam_prefix_list_resolver_targets
12406            .remove(&key)
12407            .ok_or_else(|| {
12408                Ec2Error::InvalidIpamPrefixListResolverTargetNotFound(
12409                    resolver_id.to_string(),
12410                    target_id.to_string(),
12411                )
12412            })?;
12413        removed.state = "deleted".to_string();
12414        if let Some(r) = self.ipam_prefix_list_resolvers.get_mut(resolver_id) {
12415            if r.target_count > 0 {
12416                r.target_count -= 1;
12417            }
12418        }
12419        Ok(removed)
12420    }
12421
12422    pub fn modify_ipam_prefix_list_resolver_target(
12423        &mut self,
12424        resolver_id: &str,
12425        target_id: &str,
12426        target_resource_region: Option<String>,
12427    ) -> Result<&IpamPrefixListResolverTarget, Ec2Error> {
12428        let key = (resolver_id.to_string(), target_id.to_string());
12429        let target = self
12430            .ipam_prefix_list_resolver_targets
12431            .get_mut(&key)
12432            .ok_or_else(|| {
12433                Ec2Error::InvalidIpamPrefixListResolverTargetNotFound(
12434                    resolver_id.to_string(),
12435                    target_id.to_string(),
12436                )
12437            })?;
12438        if let Some(r) = target_resource_region {
12439            target.target_resource_region = r;
12440        }
12441        target.state = "modifying".to_string();
12442        Ok(target)
12443    }
12444
12445    pub fn move_byoip_cidr_to_ipam(
12446        &mut self,
12447        cidr: &str,
12448        ipam_pool_id: &str,
12449    ) -> Result<&ByoipCidr, Ec2Error> {
12450        if !self.ipam_pools.contains_key(ipam_pool_id) {
12451            return Err(Ec2Error::InvalidIpamPoolNotFound(ipam_pool_id.to_string()));
12452        }
12453        let entry = self
12454            .byoip_cidrs
12455            .get_mut(cidr)
12456            .ok_or_else(|| Ec2Error::InvalidByoipCidrNotFound(cidr.to_string()))?;
12457        entry.ipam_pool_id = Some(ipam_pool_id.to_string());
12458        Ok(entry)
12459    }
12460
12461    // -----------------------------------------------------------------------
12462    // Batch B: ID generators and state operations.
12463    // -----------------------------------------------------------------------
12464
12465    pub fn next_bundle_task_id(&mut self) -> String {
12466        self.counters.bundle_task += 1;
12467        format!("bun-{:08x}", self.counters.bundle_task)
12468    }
12469
12470    pub fn next_import_volume_task_id(&mut self) -> String {
12471        self.counters.import_volume_task += 1;
12472        format!("import-vol-{:08x}", self.counters.import_volume_task)
12473    }
12474
12475    pub fn next_export_image_task_id(&mut self) -> String {
12476        self.counters.export_image_task += 1;
12477        format!("export-ami-{:08x}", self.counters.export_image_task)
12478    }
12479
12480    pub fn next_outpost_lag_id(&mut self) -> String {
12481        self.counters.outpost_lag += 1;
12482        format!("outpost-lag-{:08x}", self.counters.outpost_lag)
12483    }
12484
12485    /// `ModifyVolume`: mutate the volume in place and record a `VolumeModification`.
12486    #[allow(clippy::too_many_arguments)]
12487    pub fn modify_volume(
12488        &mut self,
12489        volume_id: &str,
12490        new_size: Option<i32>,
12491        new_volume_type: Option<String>,
12492        new_iops: Option<i32>,
12493        new_throughput: Option<i32>,
12494        new_multi_attach_enabled: Option<bool>,
12495    ) -> Result<&VolumeModification, Ec2Error> {
12496        let now = self.now_iso();
12497        let vol = self
12498            .volumes
12499            .get_mut(volume_id)
12500            .ok_or_else(|| Ec2Error::VolumeNotFound(volume_id.to_string()))?;
12501        let original_size = vol.size;
12502        let original_iops = vol.iops;
12503        let original_throughput = vol.throughput;
12504        let original_volume_type = vol.volume_type.clone();
12505        if let Some(s) = new_size {
12506            vol.size = s;
12507        }
12508        if let Some(ref vt) = new_volume_type {
12509            vol.volume_type = vt.clone();
12510        }
12511        if let Some(i) = new_iops {
12512            vol.iops = Some(i);
12513        }
12514        if let Some(t) = new_throughput {
12515            vol.throughput = Some(t);
12516        }
12517        let modification = VolumeModification {
12518            volume_id: volume_id.to_string(),
12519            modification_state: "completed".to_string(),
12520            status_message: None,
12521            target_size: new_size.or(Some(original_size)),
12522            target_iops: new_iops.or(original_iops),
12523            target_throughput: new_throughput.or(original_throughput),
12524            target_volume_type: new_volume_type.or(Some(original_volume_type.clone())),
12525            target_multi_attach_enabled: new_multi_attach_enabled,
12526            original_size: Some(original_size),
12527            original_iops,
12528            original_throughput,
12529            original_volume_type: Some(original_volume_type),
12530            original_multi_attach_enabled: None,
12531            progress: 100,
12532            start_time: now.clone(),
12533            end_time: Some(now),
12534        };
12535        self.counters.volume_modification += 1;
12536        self.volume_modifications
12537            .insert(volume_id.to_string(), modification);
12538        Ok(self.volume_modifications.get(volume_id).unwrap())
12539    }
12540
12541    /// `ImportVolume`: synthesise a conversion task.
12542    #[allow(clippy::too_many_arguments)]
12543    pub fn import_volume(
12544        &mut self,
12545        availability_zone: String,
12546        image_format: String,
12547        image_size: i64,
12548        image_import_manifest_url: String,
12549        volume_size: i64,
12550        description: Option<String>,
12551    ) -> &ImportVolumeTask {
12552        let id = self.next_import_volume_task_id();
12553        let now = chrono::Utc::now();
12554        let expires = (now + chrono::Duration::days(7))
12555            .format("%Y-%m-%dT%H:%M:%S.000Z")
12556            .to_string();
12557        let volume_id = format!("vol-{:08x}", self.counters.vol);
12558        let task = ImportVolumeTask {
12559            conversion_task_id: id.clone(),
12560            expiration_time: expires,
12561            image: DiskImageDescription {
12562                format: image_format,
12563                size: image_size,
12564                import_manifest_url: image_import_manifest_url,
12565                checksum: None,
12566            },
12567            volume: DiskImageVolumeDescription {
12568                size: volume_size,
12569                id: volume_id,
12570            },
12571            availability_zone,
12572            bytes_converted: 0,
12573            description,
12574            status: "active".to_string(),
12575            status_message: None,
12576        };
12577        self.import_volume_tasks.insert(id.clone(), task);
12578        self.import_volume_tasks.get(&id).unwrap()
12579    }
12580
12581    pub fn cancel_import_volume_task(&mut self, task_id: &str) -> Result<(), Ec2Error> {
12582        let task = self
12583            .import_volume_tasks
12584            .get_mut(task_id)
12585            .ok_or_else(|| Ec2Error::InvalidImportVolumeTaskNotFound(task_id.to_string()))?;
12586        task.status = "cancelled".to_string();
12587        Ok(())
12588    }
12589
12590    /// `BundleInstance`: create a bundle task.
12591    pub fn bundle_instance(
12592        &mut self,
12593        instance_id: String,
12594        bucket: String,
12595        prefix: String,
12596    ) -> &BundleTask {
12597        let id = self.next_bundle_task_id();
12598        let now = self.now_iso();
12599        let task = BundleTask {
12600            bundle_id: id.clone(),
12601            instance_id,
12602            bucket,
12603            prefix,
12604            start_time: now.clone(),
12605            update_time: now,
12606            state: "pending".to_string(),
12607            progress: "0%".to_string(),
12608            error_code: None,
12609            error_message: None,
12610        };
12611        self.bundle_tasks.insert(id.clone(), task);
12612        self.bundle_tasks.get(&id).unwrap()
12613    }
12614
12615    pub fn cancel_bundle_task(&mut self, bundle_id: &str) -> Result<&BundleTask, Ec2Error> {
12616        let now = self.now_iso();
12617        let task = self
12618            .bundle_tasks
12619            .get_mut(bundle_id)
12620            .ok_or_else(|| Ec2Error::InvalidBundleTaskNotFound(bundle_id.to_string()))?;
12621        task.state = "cancelling".to_string();
12622        task.update_time = now;
12623        Ok(self.bundle_tasks.get(bundle_id).unwrap())
12624    }
12625
12626    pub fn describe_bundle_tasks(&self, ids: &[String]) -> Vec<BundleTask> {
12627        self.bundle_tasks
12628            .values()
12629            .filter(|t| ids.is_empty() || ids.contains(&t.bundle_id))
12630            .cloned()
12631            .collect()
12632    }
12633
12634    /// `ModifyIdFormat`: set per-resource long/short ID format toggle.
12635    pub fn set_id_format(&mut self, resource: &str, use_long_ids: bool) {
12636        self.id_format.insert(
12637            resource.to_string(),
12638            IdFormatEntry {
12639                use_long_ids,
12640                deadline: None,
12641            },
12642        );
12643    }
12644
12645    /// `EnableFastLaunch`: store fast-launch state on the image.
12646    #[allow(clippy::too_many_arguments)]
12647    pub fn enable_fast_launch(
12648        &mut self,
12649        image_id: &str,
12650        resource_type: String,
12651        target_resource_count: Option<i32>,
12652        max_parallel_launches: i32,
12653        launch_template_id: Option<String>,
12654        launch_template_name: Option<String>,
12655        version: Option<String>,
12656        owner_id: String,
12657    ) -> Result<&FastLaunchState, Ec2Error> {
12658        let now = self.now_iso();
12659        let img = self
12660            .images
12661            .get_mut(image_id)
12662            .ok_or_else(|| Ec2Error::AmiNotFound(image_id.to_string()))?;
12663        img.fast_launch_state = Some(FastLaunchState {
12664            state: "enabled".to_string(),
12665            image_id: image_id.to_string(),
12666            resource_type,
12667            snapshot_configuration: SnapshotConfigurationRequest {
12668                target_resource_count,
12669            },
12670            launch_template: FastLaunchLaunchTemplateSpecification {
12671                launch_template_id,
12672                launch_template_name,
12673                version,
12674            },
12675            max_parallel_launches,
12676            owner_id,
12677            state_transition_time: now,
12678        });
12679        Ok(img.fast_launch_state.as_ref().unwrap())
12680    }
12681
12682    pub fn disable_fast_launch(&mut self, image_id: &str) -> Result<&FastLaunchState, Ec2Error> {
12683        let now = self.now_iso();
12684        let img = self
12685            .images
12686            .get_mut(image_id)
12687            .ok_or_else(|| Ec2Error::AmiNotFound(image_id.to_string()))?;
12688        match img.fast_launch_state.as_mut() {
12689            Some(fl) => {
12690                fl.state = "disabled".to_string();
12691                fl.state_transition_time = now;
12692                Ok(img.fast_launch_state.as_ref().unwrap())
12693            }
12694            None => {
12695                // AWS returns the disabled state even when fast-launch was
12696                // never enabled; synthesise a minimal record.
12697                img.fast_launch_state = Some(FastLaunchState {
12698                    state: "disabled".to_string(),
12699                    image_id: image_id.to_string(),
12700                    resource_type: "snapshot".to_string(),
12701                    snapshot_configuration: SnapshotConfigurationRequest::default(),
12702                    launch_template: FastLaunchLaunchTemplateSpecification::default(),
12703                    max_parallel_launches: 0,
12704                    owner_id: String::new(),
12705                    state_transition_time: now,
12706                });
12707                Ok(img.fast_launch_state.as_ref().unwrap())
12708            }
12709        }
12710    }
12711
12712    /// `ExportImage`: synthesise an export task.
12713    #[allow(clippy::too_many_arguments)]
12714    pub fn export_image(
12715        &mut self,
12716        image_id: String,
12717        role_name: String,
12718        s3_bucket: String,
12719        s3_prefix: Option<String>,
12720        disk_image_format: String,
12721        description: Option<String>,
12722        tags: Tags,
12723    ) -> &ExportImageTask {
12724        let id = self.next_export_image_task_id();
12725        let task = ExportImageTask {
12726            export_image_task_id: id.clone(),
12727            description,
12728            image_id,
12729            role_name,
12730            status: "active".to_string(),
12731            status_message: None,
12732            progress: "0".to_string(),
12733            s3_export_location: ExportTaskS3Location {
12734                s3_bucket,
12735                s3_prefix,
12736            },
12737            disk_image_format,
12738            tags,
12739        };
12740        self.export_image_tasks.insert(id.clone(), task);
12741        self.export_image_tasks.get(&id).unwrap()
12742    }
12743}
12744
12745/// Translate a Route Server `PersistRoutes` action ("enable" / "disable" /
12746/// "reset") to its corresponding state ("enabled" / "disabled" / "resetting").
12747/// Defaults to "disabled" when the action is `None` or unrecognised.
12748fn action_to_persist_routes_state(action: Option<&str>) -> String {
12749    match action.unwrap_or("disable") {
12750        "enable" => "enabled".to_string(),
12751        "disable" => "disabled".to_string(),
12752        "reset" => "resetting".to_string(),
12753        other => other.to_string(),
12754    }
12755}
12756
12757/// Per-family default credit pair, returned by `ModifyDefaultCreditSpecification`.
12758#[derive(Debug, Clone)]
12759pub struct InstanceFamilyCreditPair {
12760    pub instance_family: String,
12761    pub cpu_credits: String,
12762}
12763
12764/// Single purchase entry passed to `purchase_scheduled_instances`.
12765#[derive(Debug, Clone)]
12766pub struct ScheduledInstancePurchaseRequest {
12767    pub instance_type: String,
12768    pub platform: Option<String>,
12769    pub network_platform: Option<String>,
12770    pub availability_zone: String,
12771    pub instance_count: i32,
12772    pub hourly_price: Option<String>,
12773    pub total_scheduled_instance_hours: i32,
12774    pub recurrence: ScheduledInstanceRecurrence,
12775    pub slot_duration_in_hours: i32,
12776}