[Darwin] Unit test for #39003 (#39041)

* [Darwin] Unit test for #39003

* Update src/darwin/Framework/CHIPTests/MTRDeviceTests.m

Co-authored-by: Kiel Oleson <kielo@apple.com>

---------

Co-authored-by: Justin Wood <woody@apple.com>
Co-authored-by: Kiel Oleson <kielo@apple.com>
diff --git a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm
index 7f12544..fbf170e 100644
--- a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm
+++ b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm
@@ -661,6 +661,13 @@
     }];
 }
 
+#ifdef DEBUG
+- (void)unitTestSyncRunOnDeviceQueue:(dispatch_block_t)block
+{
+    dispatch_sync(self.queue, block);
+}
+#endif
+
 #pragma mark - Time Synchronization
 
 - (void)_setTimeOnDevice
@@ -1072,8 +1079,6 @@
     }
     os_unfair_lock_unlock(&self->_lock);
 
-    BOOL resubscribeScheduled = NO;
-
     if (readClientToResubscribe) {
         if (nodeLikelyReachable) {
             // If we have reason to suspect the node is now reachable, reset the
@@ -1082,7 +1087,14 @@
             // here (e.g. still booting up), but should try again reasonably quickly.
             subscriptionCallback->ResetResubscriptionBackoff();
         }
-        resubscribeScheduled = readClientToResubscribe->TriggerResubscribeIfScheduled(reason.UTF8String);
+
+        BOOL resubscribeScheduled = readClientToResubscribe->TriggerResubscribeIfScheduled(reason.UTF8String);
+
+        // In the case no resubscribe was actually scheduled, remove this device from the subscription pool
+        if (!resubscribeScheduled) {
+            std::lock_guard lock(_lock);
+            [self _clearSubscriptionPoolWork];
+        }
     } else if (((_internalDeviceState == MTRInternalDeviceStateSubscribing && !self.doingCASEAttemptForDeviceMayBeReachable) || shouldReattemptSubscription) && nodeLikelyReachable) {
         // If we have reason to suspect that the node is now reachable and we haven't established a
         // CASE session yet, let's consider it to be stalled and invalidate the pairing session.
@@ -1107,13 +1119,7 @@
     // and should be called after the above ReleaseSession call, to avoid churn.
     if (shouldReattemptSubscription) {
         std::lock_guard lock(_lock);
-        resubscribeScheduled = [self _reattemptSubscriptionNowIfNeededWithReason:reason];
-    }
-
-    // In the case no resubscribe was actually scheduled, remove this device from the subscription pool
-    if (!resubscribeScheduled) {
-        std::lock_guard lock(_lock);
-        [self _clearSubscriptionPoolWork];
+        [self _reattemptSubscriptionNowIfNeededWithReason:reason];
     }
 }
 
diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
index a3f9ed8..e1fbb65 100644
--- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
@@ -6180,6 +6180,46 @@
     delegate.onReachable = nil;
 }
 
+- (void)test048_MTRDeviceResubscribeOnSubscriptionPool
+{
+    __auto_type * device = [MTRDevice deviceWithNodeID:kDeviceId1 deviceController:sController];
+    dispatch_queue_t queue = dispatch_queue_create("subscription-pool-queue", DISPATCH_QUEUE_SERIAL);
+
+    __auto_type * delegate = [[MTRDeviceTestDelegate alloc] init];
+    delegate.pretendThreadEnabled = YES;
+    delegate.subscriptionMaxIntervalOverride = @(20); // larger than kTimeoutInSeconds so empty reports do not clear subscription pool sooner
+
+    XCTestExpectation * subscriptionExpectation = [self expectationWithDescription:@"Subscription work completed"];
+    delegate.onReportEnd = ^{
+        [subscriptionExpectation fulfill];
+    };
+
+    [device setDelegate:delegate queue:queue];
+
+    [self waitForExpectations:@[ subscriptionExpectation ] timeout:60];
+
+    // Wait for subscription report stuff to clear and _handleSubscriptionEstablished asynced to device queue
+    [sController syncRunOnWorkQueue:^{
+        ;
+    } error:nil];
+
+    // Wait for _handleSubscriptionEstablished to finish removing subscription work from pool
+    [device unitTestSyncRunOnDeviceQueue:^{
+        ;
+    }];
+
+    // Now we can set up waiting for onSubscriptionPoolWorkComplete from the test
+    XCTestExpectation * subscriptionPoolWorkCompleteForTriggerTestExpectation = [self expectationWithDescription:@"_triggerResubscribeWithReason work completed"];
+    delegate.onSubscriptionPoolWorkComplete = ^{
+        [subscriptionPoolWorkCompleteForTriggerTestExpectation fulfill];
+    };
+
+    // Now that subscription is established and live, ReadClient->mIsResubscriptionScheduled should be false, and _handleResubscriptionNeededWithDelayOnDeviceQueue can simulate the code path that leads to ReadClient->TriggerResubscribeIfScheduled() returning false, and exercise the edge case
+    [device _handleResubscriptionNeededWithDelayOnDeviceQueue:@(0)];
+
+    [self waitForExpectations:@[ subscriptionPoolWorkCompleteForTriggerTestExpectation ] timeout:kTimeoutInSeconds];
+}
+
 @end
 
 @interface MTRDeviceEncoderTests : XCTestCase
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h
index f1eb607..502e590 100644
--- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.h
@@ -39,6 +39,7 @@
 @property (nonatomic, nullable) dispatch_block_t onClusterDataPersisted;
 @property (nonatomic, nullable) dispatch_block_t onSubscriptionCallbackDelete;
 @property (nonatomic, nullable) dispatch_block_t onSubscriptionReset;
+@property (nonatomic, nullable) NSNumber * subscriptionMaxIntervalOverride;
 @end
 
 @interface MTRDeviceTestDelegateWithSubscriptionSetupOverride : MTRDeviceTestDelegate
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.m b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.m
index e239ae0..e40bfa5 100644
--- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.m
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRDeviceTestDelegate.m
@@ -63,6 +63,10 @@
 
 - (NSNumber *)unitTestMaxIntervalOverrideForSubscription:(MTRDevice *)device
 {
+    if (self.subscriptionMaxIntervalOverride) {
+        return self.subscriptionMaxIntervalOverride;
+    }
+
     // Make sure our subscriptions time out in finite time.
     return @(2); // seconds
 }
diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h
index 63df5ba..bad3ebb 100644
--- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h
+++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h
@@ -62,6 +62,7 @@
 - (NSMutableArray<NSNumber *> *)arrayOfNumbersFromAttributeValue:(MTRDeviceDataValueDictionary)dataDictionary;
 - (void)setStorageBehaviorConfiguration:(MTRDeviceStorageBehaviorConfiguration *)storageBehaviorConfiguration;
 - (void)_deviceMayBeReachable;
+- (void)_handleResubscriptionNeededWithDelayOnDeviceQueue:(NSNumber *)resubscriptionDelayMs;
 
 @property (nonatomic, readonly, nullable) NSNumber * highestObservedEventNumber;
 @property (nonatomic, readonly) MTRAsyncWorkQueue<MTRDevice *> * asyncWorkQueue;
@@ -101,6 +102,7 @@
 - (NSSet<MTRClusterPath *> *)unitTestGetPersistedClusters;
 - (BOOL)unitTestClusterHasBeenPersisted:(MTRClusterPath *)path;
 - (NSUInteger)unitTestAttributeCount;
+- (void)unitTestSyncRunOnDeviceQueue:(dispatch_block_t)block;
 @end
 #endif