[ICD]Add unit tests for the ICD Manager operational states (#28729)

* Add unit tests for the ICD Manager

* Address comment, try to fix test for ESP and IOT SDK

* Use GetIOContext().DriveIO() to run event loop. Set the systemLayer for test to be the IOContext one

* Clean up

* Restyled by whitespace

---------

Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/src/app/icd/ICDManager.cpp b/src/app/icd/ICDManager.cpp
index e3787f2..287d0dd 100644
--- a/src/app/icd/ICDManager.cpp
+++ b/src/app/icd/ICDManager.cpp
@@ -75,9 +75,12 @@
 
 bool ICDManager::SupportsCheckInProtocol()
 {
-    bool success;
-    uint32_t featureMap;
+    bool success        = false;
+    uint32_t featureMap = 0;
+    // Can't use attribute accessors/Attributes::FeatureMap::Get in unit tests
+#ifndef CONFIG_BUILD_FOR_HOST_UNIT_TEST
     success = (Attributes::FeatureMap::Get(kRootEndpointId, &featureMap) == EMBER_ZCL_STATUS_SUCCESS);
+#endif
     return success ? ((featureMap & to_underlying(Feature::kCheckInProtocolSupport)) != 0) : false;
 }
 
diff --git a/src/app/icd/ICDManager.h b/src/app/icd/ICDManager.h
index e5406f1..6e98902 100644
--- a/src/app/icd/ICDManager.h
+++ b/src/app/icd/ICDManager.h
@@ -26,6 +26,10 @@
 namespace chip {
 namespace app {
 
+// Forward declaration of TestICDManager to allow it to be friend with ICDManager
+// Used in unit tests
+class TestICDManager;
+
 /**
  * @brief ICD Manager is responsible of processing the events and triggering the correct action for an ICD
  */
@@ -67,6 +71,8 @@
     static System::Clock::Milliseconds32 GetFastPollingInterval() { return kFastPollingInterval; }
 
 protected:
+    friend class TestICDManager;
+
     static void OnIdleModeDone(System::Layer * aLayer, void * appState);
     static void OnActiveModeDone(System::Layer * aLayer, void * appState);
 
diff --git a/src/app/tests/TestICDManager.cpp b/src/app/tests/TestICDManager.cpp
index 6fdaeed..81739ae 100644
--- a/src/app/tests/TestICDManager.cpp
+++ b/src/app/tests/TestICDManager.cpp
@@ -15,17 +15,199 @@
  *    See the License for the specific language governing permissions and
  *    limitations under the License.
  */
+#include <app/EventManagement.h>
+#include <app/tests/AppTestContext.h>
+#include <lib/support/TestPersistentStorageDelegate.h>
+#include <lib/support/UnitTestContext.h>
 #include <lib/support/UnitTestRegistration.h>
 #include <nlunit-test.h>
+#include <system/SystemLayerImpl.h>
 
-int TestICDManager()
+#include <app/icd/ICDManager.h>
+#include <app/icd/ICDStateObserver.h>
+#include <app/icd/IcdManagementServer.h>
+
+using namespace chip;
+using namespace chip::app;
+using namespace chip::System;
+
+namespace {
+
+class TestICDStateObserver : public app::ICDStateObserver
 {
-    static nlTest sTests[] = { NL_TEST_SENTINEL() };
+public:
+    void OnEnterActiveMode() {}
+};
 
-    nlTestSuite cmSuite = { "TestICDManager", &sTests[0], nullptr, nullptr };
+TestICDStateObserver mICDStateObserver;
+static Clock::Internal::MockClock gMockClock;
+static Clock::ClockBase * gRealClock;
 
-    nlTestRunner(&cmSuite, nullptr);
-    return (nlTestRunnerStats(&cmSuite));
+class TestContext : public Test::AppContext
+{
+public:
+    static int Initialize(void * context)
+    {
+        if (AppContext::Initialize(context) != SUCCESS)
+            return FAILURE;
+
+        auto * ctx = static_cast<TestContext *>(context);
+        DeviceLayer::SetSystemLayerForTesting(&ctx->GetSystemLayer());
+
+        gRealClock = &SystemClock();
+        Clock::Internal::SetSystemClockForTesting(&gMockClock);
+
+        if (ctx->mEventCounter.Init(0) != CHIP_NO_ERROR)
+        {
+            return FAILURE;
+        }
+
+        ctx->mICDManager.Init(&ctx->testStorage, &ctx->GetFabricTable(), &mICDStateObserver);
+        return SUCCESS;
+    }
+
+    static int Finalize(void * context)
+    {
+        auto * ctx = static_cast<TestContext *>(context);
+        ctx->mICDManager.Shutdown();
+        app::EventManagement::DestroyEventManagement();
+        System::Clock::Internal::SetSystemClockForTesting(gRealClock);
+        DeviceLayer::SetSystemLayerForTesting(nullptr);
+
+        if (AppContext::Finalize(context) != SUCCESS)
+            return FAILURE;
+
+        return SUCCESS;
+    }
+
+    app::ICDManager mICDManager;
+
+private:
+    TestPersistentStorageDelegate testStorage;
+    MonotonicallyIncreasingCounter<EventNumber> mEventCounter;
+};
+
+} // namespace
+
+namespace chip {
+namespace app {
+class TestICDManager
+{
+public:
+    /*
+     * Advance the test Mock clock time by the amout passed in argument
+     * and then force the SystemLayer Timer event loop. It will check for any expired timer,
+     * and invoke their callbacks if there are any.
+     *
+     * @param time_ms: Value in milliseconds.
+     */
+    static void AdvanceClockAndRunEventLoop(TestContext * ctx, uint32_t time_ms)
+    {
+        gMockClock.AdvanceMonotonic(System::Clock::Timeout(time_ms));
+        ctx->GetIOContext().DriveIO();
+    }
+
+    static void TestICDModeIntervals(nlTestSuite * aSuite, void * aContext)
+    {
+        TestContext * ctx = static_cast<TestContext *>(aContext);
+
+        // After the init we should be in active mode
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode);
+        AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() + 1);
+        // Active mode interval expired, ICDManager transitioned to the IdleMode.
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::IdleMode);
+        AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetIdleModeInterval() + 1);
+        // Idle mode interval expired, ICDManager transitioned to the ActiveMode.
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode);
+
+        // Events updating the Operation to Active mode can extend the current active mode time by 1 Active mode threshold.
+        // Kick an active Threshold just before the end of the Active interval and validate that the active mode is extended.
+        AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() - 1);
+        ctx->mICDManager.UpdateOperationState(ICDManager::OperationalState::ActiveMode);
+        AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeThreshold() / 2);
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode);
+        AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeThreshold());
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::IdleMode);
+    }
+
+    static void TestKeepActivemodeRequests(nlTestSuite * aSuite, void * aContext)
+    {
+        TestContext * ctx = static_cast<TestContext *>(aContext);
+
+        // Setting a requirement will transition the ICD to active mode.
+        ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kCommissioningWindowOpen, true);
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode);
+        // Advance time so active mode interval expires.
+        AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() + 1);
+        // Requirement flag still set. We stay in active mode
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode);
+
+        // Remove requirement. we should directly transition to idle mode.
+        ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kCommissioningWindowOpen, false);
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::IdleMode);
+
+        ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kFailSafeArmed, true);
+        // Requirement will transition us to active mode.
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode);
+
+        // Advance time, but by less than the active mode interval and remove the requirement.
+        // We should stay in active mode.
+        AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() / 2);
+        ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kFailSafeArmed, false);
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode);
+
+        // Advance time again, The activemode interval is completed.
+        AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() + 1);
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::IdleMode);
+
+        // Set two requirements
+        ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kExpectingMsgResponse, true);
+        ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kAwaitingMsgAck, true);
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode);
+        // advance time so the active mode interval expires.
+        AdvanceClockAndRunEventLoop(ctx, IcdManagementServer::GetInstance().GetActiveModeInterval() + 1);
+        // A requirement flag is still set. We stay in active mode.
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode);
+
+        // remove 1 requirement. Active mode is maintained
+        ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kExpectingMsgResponse, false);
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::ActiveMode);
+        // remove the last requirement
+        ctx->mICDManager.SetKeepActiveModeRequirements(ICDManager::KeepActiveFlags::kAwaitingMsgAck, false);
+        NL_TEST_ASSERT(aSuite, ctx->mICDManager.mOperationalState == ICDManager::OperationalState::IdleMode);
+    }
+};
+
+} // namespace app
+} // namespace chip
+
+namespace {
+/**
+ *   Test Suite. It lists all the test functions.
+ */
+// clang-format off
+static const nlTest sTests[] =
+{
+    NL_TEST_DEF("TestICDModeIntervals",         TestICDManager::TestICDModeIntervals),
+    NL_TEST_DEF("TestKeepActivemodeRequests",   TestICDManager::TestKeepActivemodeRequests),
+    NL_TEST_SENTINEL()
+};
+// clang-format on
+
+// clang-format off
+nlTestSuite cmSuite =
+{
+    "TestICDManager",
+    &sTests[0],
+    TestContext::Initialize,
+    TestContext::Finalize
+};
+// clang-format on
+} // namespace
+
+int TestSuiteICDManager()
+{
+    return ExecuteTestsWithContext<TestContext>(&cmSuite);
 }
 
-CHIP_REGISTER_TEST_SUITE(TestICDManager)
+CHIP_REGISTER_TEST_SUITE(TestSuiteICDManager)