rust/bt-ascs: Implement Receiver Stop Ready operation

This change implements the client-side "Receiver Stop Ready" operation
as defined in ASCS v1.0.1, Section 5.6.

 - Add the `receiver_stop_ready` method to `AudioStreamEndpointHandle`.
 - Update `AseControlOperation` to handle encoding and state transitions.
 - Add unit tests for success and various failure scenarios.

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

Change-Id: I5c5482b6271d869a70f04671a122e2f86d91430c
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2481
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs
index 71a4192..20131be 100644
--- a/rust/bt-ascs/src/client.rs
+++ b/rust/bt-ascs/src/client.rs
@@ -328,6 +328,30 @@
         Ok(())
     }
 
+    fn verify_ase_directions<'a>(
+        &self,
+        pending_ases: impl IntoIterator<Item = &'a AseId>,
+        opcode: AseControlPointOpcode,
+        expected: AudioDirection,
+    ) -> Result<(), Error> {
+        for &ase_id in pending_ases {
+            let direction = self
+                .endpoints
+                .lookup_by_ase_id(ase_id)
+                .ok_or(Error::Client(ClientError::UnknownAseId(ase_id)))?
+                .endpoint
+                .direction;
+            if direction != expected {
+                return Err(Error::Client(ClientError::InvalidDirection {
+                    ase_id,
+                    opcode,
+                    direction,
+                }));
+            }
+        }
+        Ok(())
+    }
+
     /// Encodes an ASE control operation and writes it to the ASE Control Point
     /// characteristic.
     async fn write_operation(&mut self, operation: &AseControlOperation) -> Result<(), Error> {
@@ -537,20 +561,11 @@
     ) -> Result<AseControlOperationOutcome, Error> {
         let pending_ases: HashSet<AseId> = ases.iter().cloned().collect();
         self.validate_arguments(AseControlPointOpcode::ReceiverStartReady, &pending_ases)?;
-
-        for ase_id in &pending_ases {
-            let endpoint = self
-                .endpoints
-                .lookup_by_ase_id(*ase_id)
-                .expect("ASE ID verified by validate_arguments");
-            if endpoint.endpoint.direction != AudioDirection::Source {
-                return Err(Error::Client(ClientError::InvalidDirection {
-                    ase_id: *ase_id,
-                    opcode: AseControlPointOpcode::ReceiverStartReady,
-                    direction: endpoint.endpoint.direction,
-                }));
-            }
-        }
+        self.verify_ase_directions(
+            &pending_ases,
+            AseControlPointOpcode::ReceiverStartReady,
+            AudioDirection::Source,
+        )?;
 
         let op = AseControlOperation::ReceiverStartReady { ases };
 
@@ -573,6 +588,31 @@
 
         self.perform_and_verify_operation(op, pending_ases).await
     }
+
+    /// Performs the Receiver Stop Ready operation on one or more Source ASEs.
+    ///
+    /// # Arguments
+    /// * `ases` - A vector of `AseId`s to stop.
+    ///
+    /// # Returns
+    /// On success, returns an [`AseControlOperationOutcome`] containing the
+    /// results of the operation.
+    pub async fn receiver_stop_ready(
+        &mut self,
+        ases: Vec<AseId>,
+    ) -> Result<AseControlOperationOutcome, Error> {
+        let pending_ases: HashSet<AseId> = ases.iter().cloned().collect();
+        self.validate_arguments(AseControlPointOpcode::ReceiverStopReady, &pending_ases)?;
+        self.verify_ase_directions(
+            &pending_ases,
+            AseControlPointOpcode::ReceiverStopReady,
+            AudioDirection::Source,
+        )?;
+
+        let op = AseControlOperation::ReceiverStopReady { ases };
+
+        self.perform_and_verify_operation(op, pending_ases).await
+    }
 }
 
 #[cfg(test)]
@@ -1680,4 +1720,139 @@
             })
         ));
     }
+
+    #[test]
+    fn receiver_stop_ready_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 SOURCE_ASE_HANDLE (ASE 2) to Disabling state
+        let source_value = vec![
+            0x02, // ASE ID: 2
+            0x05, // ASE State: Disabling
+            0x01, 0x01, 0x00,
+        ];
+        client.endpoints.source.get_mut(&SOURCE_ASE_HANDLE).unwrap().endpoint =
+            AudioStreamEndpoint::from_char_value(
+                SOURCE_ASE_HANDLE,
+                AudioDirection::Source,
+                &source_value,
+            )
+            .unwrap();
+
+        #[rustfmt::skip]
+        service.expect_characteristic_value(
+            &CONTROL_POINT_HANDLE,
+            vec![
+                0x06, // Opcode: Receiver Stop Ready
+                0x01, // Num ASEs
+                0x02, // ASE ID: 2
+            ],
+        );
+
+        #[rustfmt::skip]
+        service.notify(
+            &CONTROL_POINT_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: CONTROL_POINT_HANDLE,
+                value: vec![
+                    0x06, // Opcode: Receiver Stop Ready
+                    0x01, // Num ASEs
+                    0x02, 0x00, 0x00, // ASE ID: 2, Success
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        // Expected state transitions back to QoS Configured
+        #[rustfmt::skip]
+        service.notify(
+            &SOURCE_ASE_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: SOURCE_ASE_HANDLE,
+                value: vec![
+                    0x02, // ASE ID: 2
+                    0x02, // ASE State: QoS Configured
+                    0x01, 0x01, 0x10, 0x27, 0x00, 0x00, 0x01, 0x64, 0x00, 0x02, 0x0A, 0x00, 0x40, 0x9C, 0x00,
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        let stop_fut = client.receiver_stop_ready(vec![AseId(2)]);
+        let outcome = run_to_completion(stop_fut).expect("receiver stop ready should succeed");
+
+        assert_eq!(outcome.rejected().len(), 0);
+        assert_eq!(
+            client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state,
+            AseState::QosConfigured
+        );
+    }
+
+    #[test]
+    fn receiver_stop_ready_fail_invalid_direction() {
+        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");
+
+        // Pre-condition SINK_ASE_HANDLE (ASE 1) to Disabling (or other state)
+        let sink_value = vec![
+            0x01, // ASE ID: 1
+            0x05, // ASE State: Disabling
+            0x01, 0x01, 0x00,
+        ];
+        client.endpoints.sink.get_mut(&SINK_ASE_HANDLE).unwrap().endpoint =
+            AudioStreamEndpoint::from_char_value(
+                SINK_ASE_HANDLE,
+                AudioDirection::Sink,
+                &sink_value,
+            )
+            .unwrap();
+
+        let stop_fut = client.receiver_stop_ready(vec![AseId(1)]);
+        let err = run_to_completion(stop_fut).expect_err("should fail client-side validation");
+
+        assert!(matches!(
+            err,
+            Error::Client(ClientError::InvalidDirection {
+                ase_id: AseId(1),
+                opcode: AseControlPointOpcode::ReceiverStopReady,
+                direction: AudioDirection::Sink,
+            })
+        ));
+    }
+
+    #[test]
+    fn receiver_stop_ready_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");
+
+        // Source ASE is in Idle state, which is invalid starting state for
+        // ReceiverStopReady
+        let stop_fut = client.receiver_stop_ready(vec![AseId(2)]);
+        let err = run_to_completion(stop_fut).expect_err("should fail client-side validation");
+
+        assert!(matches!(
+            err,
+            Error::Client(ClientError::InvalidStartState {
+                ase_id: AseId(2),
+                opcode: AseControlPointOpcode::ReceiverStopReady,
+                actual: AseState::Idle,
+            })
+        ));
+    }
+
+    #[test]
+    fn receiver_stop_ready_fail_unknown_ase_id() {
+        let service = setup_fake_service();
+        let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service);
+        let mut client = run_to_completion(client_fut).expect("client creation should succeed");
+
+        let stop_fut = client.receiver_stop_ready(vec![AseId(99)]);
+        let err = run_to_completion(stop_fut).expect_err("should fail for unknown ASE ID");
+
+        assert!(matches!(err, Error::Client(ClientError::UnknownAseId(AseId(99)))));
+    }
 }