rust/bt-ascs: Implement Enable operation

This change implements the "Enable" operation for the ASCS client.

The implementation includes:
 - A new `enable` method on the `AudioStreamEndpointHandle`.
 - Unit tests for success and failure scenarios.
 - A new unit test for the `Enable` decoding logic.

Test: cargo test -p bt-ascs
Bug: b/431814103

Change-Id: I19f47179aaffaf529c63791f338a8d4a1e6bcb5e
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2420
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs
index 90a64c4..6b7fac7 100644
--- a/rust/bt-ascs/src/client.rs
+++ b/rust/bt-ascs/src/client.rs
@@ -542,6 +542,32 @@
 
         self.perform_and_verify_operation(op, pending_ases).await
     }
+
+    /// Performs the Enable operation on one or more ASEs.
+    ///
+    /// This method sends the Enable request to the remote server,
+    /// waits for control point notifications for all involved ASEs, and
+    /// verifies that the successfully enabled ASEs have transitioned to
+    /// the `Enabling` state.
+    ///
+    /// # Arguments
+    /// * `ases_with_metadata` - A vector of `AseIdWithMetadata` structs
+    ///   specifying the target ASE IDs and their codec metadata.
+    ///
+    /// # Returns
+    /// On success, returns a `Vec<ResponseCode>` containing the response
+    /// codes for any ASEs where the enable operation failed.
+    pub async fn enable(
+        &mut self,
+        ases_with_metadata: Vec<AseIdWithMetadata>,
+    ) -> Result<AseControlOperationOutcome, Error> {
+        let pending_ases: HashSet<AseId> = ases_with_metadata.iter().map(|a| a.ase_id).collect();
+        self.validate_arguments(AseControlPointOpcode::Enable, &pending_ases)?;
+
+        let op = AseControlOperation::Enable { ases_with_metadata, responses: vec![] };
+
+        self.perform_and_verify_operation(op, pending_ases).await
+    }
 }
 
 #[cfg(test)]
@@ -1244,4 +1270,112 @@
         );
         assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Streaming);
     }
+
+    #[test]
+    fn enable_success() {
+        let mut service = setup_fake_service();
+        let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service.clone());
+        let mut client = run_to_completion(client_fut).expect("client creation should succeed");
+
+        // Pre-condition SINK_ASE_HANDLE to QosConfigured state
+        let sink_value = vec![
+            0x01, // ASE ID: 1
+            0x02, // ASE State: QoS Configured
+            0x01, 0x01, 0x10, 0x27, 0x00, 0x00, 0x01, 0x64, 0x00, 0x02, 0x0A, 0x00, 0x40, 0x9C,
+            0x00,
+        ];
+        client.endpoints.sink.get_mut(&SINK_ASE_HANDLE).unwrap().endpoint =
+            AudioStreamEndpoint::from_char_value(
+                SINK_ASE_HANDLE,
+                AudioDirection::Sink,
+                &sink_value,
+            )
+            .unwrap();
+
+        let enable_req = AseIdWithMetadata { ase_id: AseId(1), metadata: vec![] };
+
+        #[rustfmt::skip]
+        service.expect_characteristic_value(
+            &CONTROL_POINT_HANDLE,
+            vec![
+                0x03, // Opcode: Enable
+                0x01, // Num ASEs
+                0x01, // ASE ID: 1
+                0x00, // Metadata Length: 0
+            ],
+        );
+
+        #[rustfmt::skip]
+        service.notify(
+            &CONTROL_POINT_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: CONTROL_POINT_HANDLE,
+                value: vec![
+                    0x03, // Opcode: Enable
+                    0x01, // Num ASEs
+                    0x01, 0x00, 0x00, // ASE ID: 1, Success
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        #[rustfmt::skip]
+        service.notify(
+            &SINK_ASE_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: SINK_ASE_HANDLE,
+                value: vec![
+                    0x01, // ASE ID: 1
+                    0x03, // ASE State: Enabling
+                    0x01, // CIG ID: 1
+                    0x01, // CIS ID: 1
+                    0x00, // Metadata Length: 0
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        let enable_fut = client.enable(vec![enable_req]);
+        let outcome = run_to_completion(enable_fut).expect("enable should succeed");
+
+        assert_eq!(outcome.rejected().len(), 0);
+        assert_eq!(client.endpoints.sink[&SINK_ASE_HANDLE].endpoint.state, AseState::Enabling);
+    }
+
+    #[test]
+    fn enable_fail_invalid_start_state() {
+        let service = setup_fake_service();
+        let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service.clone());
+        let mut client = run_to_completion(client_fut).expect("client creation should succeed");
+
+        // Transition the sink ASE to Streaming state to test client-side validation.
+        let streaming_value = vec![
+            0x01, // ASE ID: 1
+            0x04, // ASE State: Streaming
+            0x01, // CIG ID: 1
+            0x01, // CIS ID: 1
+            0x00, // Metadata Length: 0
+        ];
+        client.endpoints.sink.get_mut(&SINK_ASE_HANDLE).unwrap().endpoint =
+            AudioStreamEndpoint::from_char_value(
+                SINK_ASE_HANDLE,
+                AudioDirection::Sink,
+                &streaming_value,
+            )
+            .unwrap();
+
+        let enable_req = AseIdWithMetadata { ase_id: AseId(1), metadata: vec![] };
+
+        let enable_fut = client.enable(vec![enable_req]);
+        let err = run_to_completion(enable_fut).expect_err("should fail client-side validation");
+
+        assert!(matches!(
+            err,
+            Error::Client(ClientError::InvalidStartState {
+                ase_id: AseId(1),
+                opcode: AseControlPointOpcode::Enable,
+                actual: AseState::Streaming,
+            })
+        ));
+    }
 }