[OPSTATE] Add Q test script for CountdownTime (#34632)

* Add Q test

* Added test to test set

* Remove unused var

* Restyled by autopep8

* Restyled by isort

* Fix name

* Use pics over other method

* Removed unused stuff

* Added pipe commands

* Fix reset

* Get example to report appropriate changes.

* WiP

* Added some comments

* Changes to make things work

* Removed dev msgs

* Missed some

* Removed dev msgs

* Straggler

* Restyled by clang-format

* Restyled by autopep8

* Restyled by isort

* Commented unused var

* Update examples/all-clusters-app/linux/AllClustersCommandDelegate.cpp

* Fix bug

---------

Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 8284b5a..5e38c6c 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -578,6 +578,7 @@
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OPSTATE_2_3.py'
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OPSTATE_2_4.py'
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OPSTATE_2_5.py'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OPSTATE_2_6.py'
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OVENOPSTATE_2_1.py'
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OVENOPSTATE_2_2.py'
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OVENOPSTATE_2_3.py'
diff --git a/examples/all-clusters-app/all-clusters-common/include/operational-state-delegate-impl.h b/examples/all-clusters-app/all-clusters-common/include/operational-state-delegate-impl.h
index 60b6b09..badadd6 100644
--- a/examples/all-clusters-app/all-clusters-common/include/operational-state-delegate-impl.h
+++ b/examples/all-clusters-app/all-clusters-common/include/operational-state-delegate-impl.h
@@ -138,6 +138,7 @@
 };
 
 Instance * GetOperationalStateInstance();
+OperationalStateDelegate * GetOperationalStateDelegate();
 
 void Shutdown();
 
diff --git a/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp b/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp
index d258b82..d4a91cf 100644
--- a/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp
+++ b/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp
@@ -59,6 +59,7 @@
     auto error = GetInstance()->SetOperationalState(to_underlying(OperationalState::OperationalStateEnum::kPaused));
     if (error == CHIP_NO_ERROR)
     {
+        GetInstance()->UpdateCountdownTimeFromDelegate();
         err.Set(to_underlying(ErrorStateEnum::kNoError));
     }
     else
@@ -73,6 +74,7 @@
     auto error = GetInstance()->SetOperationalState(to_underlying(OperationalStateEnum::kRunning));
     if (error == CHIP_NO_ERROR)
     {
+        GetInstance()->UpdateCountdownTimeFromDelegate();
         err.Set(to_underlying(ErrorStateEnum::kNoError));
     }
     else
@@ -96,6 +98,7 @@
     auto error = GetInstance()->SetOperationalState(to_underlying(OperationalStateEnum::kRunning));
     if (error == CHIP_NO_ERROR)
     {
+        GetInstance()->UpdateCountdownTimeFromDelegate();
         (void) DeviceLayer::SystemLayer().StartTimer(System::Clock::Seconds16(1), onOperationalStateTimerTick, this);
         err.Set(to_underlying(ErrorStateEnum::kNoError));
     }
@@ -113,6 +116,8 @@
     {
         (void) DeviceLayer::SystemLayer().CancelTimer(onOperationalStateTimerTick, this);
 
+        GetInstance()->UpdateCountdownTimeFromDelegate();
+
         OperationalState::GenericOperationalError current_err(to_underlying(OperationalState::ErrorStateEnum::kNoError));
         GetInstance()->GetCurrentOperationalError(current_err);
 
@@ -152,6 +157,11 @@
             delegate->mPausedTime++;
         }
     }
+    else if (!countdown_time.IsNull() && countdown_time.Value() <= 0)
+    {
+        OperationalState::GenericOperationalError noError(to_underlying(OperationalState::ErrorStateEnum::kNoError));
+        delegate->HandleStopStateCallback(noError);
+    }
 
     if (state == OperationalState::OperationalStateEnum::kRunning || state == OperationalState::OperationalStateEnum::kPaused)
     {
@@ -173,6 +183,11 @@
     return gOperationalStateInstance;
 }
 
+OperationalStateDelegate * OperationalState::GetOperationalStateDelegate()
+{
+    return gOperationalStateDelegate;
+}
+
 void OperationalState::Shutdown()
 {
     if (gOperationalStateInstance != nullptr)
diff --git a/examples/all-clusters-app/linux/AllClustersCommandDelegate.cpp b/examples/all-clusters-app/linux/AllClustersCommandDelegate.cpp
index d620731..6c1467f 100644
--- a/examples/all-clusters-app/linux/AllClustersCommandDelegate.cpp
+++ b/examples/all-clusters-app/linux/AllClustersCommandDelegate.cpp
@@ -667,21 +667,65 @@
 
 void AllClustersAppCommandHandler::OnOperationalStateChange(std::string device, std::string operation, Json::Value param)
 {
-    OperationalState::Instance * operationalStateInstance = nullptr;
     if (device == "Generic")
     {
-        operationalStateInstance = OperationalState::GetOperationalStateInstance();
+        OnGenericOperationalStateChange(device, operation, param);
     }
     else if (device == "Oven")
     {
-        operationalStateInstance = OvenCavityOperationalState::GetOperationalStateInstance();
+        OnOvenOperationalStateChange(device, operation, param);
     }
     else
     {
         ChipLogDetail(NotSpecified, "Invalid device type : %s", device.c_str());
         return;
     }
+}
 
+void AllClustersAppCommandHandler::OnGenericOperationalStateChange(std::string device, std::string operation, Json::Value param)
+{
+    OperationalState::Instance * operationalStateInstance                 = OperationalState::GetOperationalStateInstance();
+    OperationalState::OperationalStateDelegate * operationalStateDelegate = OperationalState::GetOperationalStateDelegate();
+    OperationalState::GenericOperationalError noError(to_underlying(OperationalState::ErrorStateEnum::kNoError));
+    OperationalState::OperationalStateEnum state =
+        static_cast<OperationalState::OperationalStateEnum>(operationalStateInstance->GetCurrentOperationalState());
+    if (operation == "Start")
+    {
+        operationalStateDelegate->HandleStartStateCallback(noError);
+    }
+    else if (operation == "Resume")
+    {
+        operationalStateDelegate->HandleResumeStateCallback(noError);
+    }
+    else if (operation == "Pause")
+    {
+        operationalStateDelegate->HandlePauseStateCallback(noError);
+    }
+    else if (operation == "Stop" && state == OperationalState::OperationalStateEnum::kRunning)
+    {
+        operationalStateDelegate->HandleStopStateCallback(noError);
+    }
+    else if (operation == "OnFault")
+    {
+        uint8_t event_id = to_underlying(OperationalState::ErrorStateEnum::kUnableToCompleteOperation);
+        if (!param.isNull())
+        {
+            event_id = to_underlying(static_cast<OperationalState::ErrorStateEnum>(param.asUInt()));
+        }
+
+        OperationalState::GenericOperationalError err(event_id);
+        operationalStateInstance->OnOperationalErrorDetected(err);
+    }
+    else
+    {
+        ChipLogDetail(NotSpecified, "Invalid operation : %s", operation.c_str());
+        return;
+    }
+}
+
+void AllClustersAppCommandHandler::OnOvenOperationalStateChange(std::string device, std::string operation, Json::Value param)
+{
+    OperationalState::Instance * operationalStateInstance = OvenCavityOperationalState::GetOperationalStateInstance();
     if (operation == "Start" || operation == "Resume")
     {
         operationalStateInstance->SetOperationalState(to_underlying(OperationalState::OperationalStateEnum::kRunning));
diff --git a/examples/all-clusters-app/linux/AllClustersCommandDelegate.h b/examples/all-clusters-app/linux/AllClustersCommandDelegate.h
index f097c53..f1b873f 100644
--- a/examples/all-clusters-app/linux/AllClustersCommandDelegate.h
+++ b/examples/all-clusters-app/linux/AllClustersCommandDelegate.h
@@ -105,6 +105,16 @@
      * Should be called when it is necessary to change the operational state as a manual operation.
      */
     void OnOperationalStateChange(std::string device, std::string operation, Json::Value param);
+
+    /**
+     * Should be called when it is necessary to change the operational state as a manual operation.
+     */
+    void OnGenericOperationalStateChange(std::string device, std::string operation, Json::Value param);
+
+    /**
+     * Should be called when it is necessary to change the operational state as a manual operation.
+     */
+    void OnOvenOperationalStateChange(std::string device, std::string operation, Json::Value param);
 };
 
 class AllClustersCommandDelegate : public NamedPipeCommandDelegate
diff --git a/src/python_testing/TC_OPSTATE_2_6.py b/src/python_testing/TC_OPSTATE_2_6.py
new file mode 100644
index 0000000..8560452
--- /dev/null
+++ b/src/python_testing/TC_OPSTATE_2_6.py
@@ -0,0 +1,62 @@
+#
+#    Copyright (c) 2024 Project CHIP Authors
+#    All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments
+# for details about the block below.
+#
+# === BEGIN CI TEST ARGUMENTS ===
+# test-runner-runs: run1
+# test-runner-run/run1/app: ${ALL_CLUSTERS_APP}
+# test-runner-run/run1/factoryreset: True
+# test-runner-run/run1/quiet: True
+# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json
+# test-runner-run/run1/script-args: --endpoint 1 --int-arg PIXIT.WAITTIME.REBOOT:5 --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+# === END CI TEST ARGUMENTS ===
+
+
+import chip.clusters as Clusters
+from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+from TC_OpstateCommon import TC_OPSTATE_BASE, TestInfo
+
+
+class TC_OPSTATE_2_6(MatterBaseTest, TC_OPSTATE_BASE):
+    def __init__(self, *args):
+        super().__init__(*args)
+
+        test_info = TestInfo(
+            pics_code="OPSTATE",
+            cluster=Clusters.OperationalState
+        )
+
+        super().setup_base(test_info=test_info)
+
+    def steps_TC_OPSTATE_2_6(self) -> list[TestStep]:
+        return self.steps_TC_OPSTATE_BASE_2_6()
+
+    def pics_TC_OPSTATE_2_6(self) -> list[str]:
+        return ["OPSTATE.S", "OPSTATE.S.A0002"]
+
+    @async_test_body
+    async def test_TC_OPSTATE_2_6(self):
+        # endpoint = self.matter_test_config.endpoint
+
+        # await self.TEST_TC_OPSTATE_BASE_2_6(endpoint=endpoint)
+        await self.TEST_TC_OPSTATE_BASE_2_6(endpoint=1)
+
+
+if __name__ == "__main__":
+    default_matter_test_main()
diff --git a/src/python_testing/TC_OpstateCommon.py b/src/python_testing/TC_OpstateCommon.py
index 09dc73e..ece50ac 100644
--- a/src/python_testing/TC_OpstateCommon.py
+++ b/src/python_testing/TC_OpstateCommon.py
@@ -28,7 +28,7 @@
 from chip.clusters.Attribute import EventReadResult, SubscriptionTransaction
 from chip.clusters.Types import NullValue
 from chip.interaction_model import InteractionModelError, Status
-from matter_testing_support import EventChangeCallback, TestStep
+from matter_testing_support import ClusterAttributeChangeAccumulator, EventChangeCallback, TestStep
 from mobly import asserts
 
 
@@ -1082,6 +1082,7 @@
 
             # STEP 7: TH waits for initial-countdown-time
             self.step(7)
+            logging.info(f'Sleeping for {initial_countdown_time:.1f} seconds.')
             time.sleep(initial_countdown_time)
 
             # STEP 8: TH sends Stop command to the DUT
@@ -1221,3 +1222,112 @@
             self.skip_step(20)
             self.skip_step(21)
             self.skip_step(22)
+
+    ############################
+    #   TEST CASE 2.6 - Optional Reports with DUT as Server
+    ############################
+    def steps_TC_OPSTATE_BASE_2_6(self) -> list[TestStep]:
+        steps = [TestStep(1, "Commissioning, already done", is_commissioning=True),
+                 TestStep(2, "Subscribe to CountdownTime attribute"),
+                 TestStep(3, "Manually put the DUT into a state where it will use the CountdownTime attribute, "
+                             "the initial value of the CountdownTime is greater than 30, "
+                             "and it will begin counting down the CountdownTime attribute."),
+                 TestStep(4, "Over a period of 30 seconds, TH counts all report transactions with an attribute "
+                             "report for the CountdownTime attribute in numberOfReportsReceived"),
+                 TestStep(5, "Until the current operation finishes, TH counts all report transactions with "
+                             "an attribute report for the CountdownTime attribute in numberOfReportsReceived and saves up to 5 such reports."),
+                 TestStep(6, "Manually put the DUT into a state where it will use the CountdownTime attribute, "
+                             "the initial value of the CountdownTime is greater than 30, and it will begin counting down the CountdownTime attribute."),
+                 TestStep(7, "TH reads from the DUT the OperationalState attribute"),
+                 TestStep(8, "Manually put the device in the Paused(0x02) operational state")
+                 ]
+        return steps
+
+    async def TEST_TC_OPSTATE_BASE_2_6(self, endpoint=1):
+        cluster = self.test_info.cluster
+        attributes = cluster.Attributes
+
+        self.init_test()
+
+        # commission
+        self.step(1)
+
+        # Note that this does a subscribe-all instead of subscribing only to the CountdownTime attribute.
+        # To-Do: Update the TP to subscribe-all.
+        self.step(2)
+        sub_handler = ClusterAttributeChangeAccumulator(cluster)
+        await sub_handler.start(self.default_controller, self.dut_node_id, endpoint)
+
+        self.step(3)
+        if self.pics_guard(self.check_pics(f"{self.test_info.pics_code}.S.M.ST_RUNNING")):
+            self.send_manual_or_pipe_command(name="OperationalStateChange",
+                                             device=self.device,
+                                             operation="Start")
+            time.sleep(1)
+            await self.read_and_expect_value(endpoint=endpoint,
+                                             attribute=attributes.OperationalState,
+                                             expected_value=cluster.Enums.OperationalStateEnum.kRunning)
+            count = sub_handler.attribute_report_counts[attributes.CountdownTime]
+            asserts.assert_greater(count, 0, "Did not receive any reports for CountdownTime")
+        else:
+            self.skip_step(3)
+
+        sub_handler.reset()
+        self.step(4)
+        logging.info('Test will now collect data for 30 seconds')
+        time.sleep(30)
+
+        count = sub_handler.attribute_report_counts[attributes.CountdownTime]
+        sub_handler.reset()
+        asserts.assert_less_equal(count, 5, "Received more than 5 reports for CountdownTime")
+        asserts.assert_greater_equal(count, 0, "Did not receive any reports for CountdownTime")
+
+        attr_value = await self.read_expect_success(
+            endpoint=endpoint,
+            attribute=attributes.OperationalState)
+        if attr_value == cluster.Enums.OperationalStateEnum.kRunning:
+            self.step(5)
+            wait_count = 0
+            while (attr_value != cluster.Enums.OperationalStateEnum.kStopped) and (wait_count < 20):
+                time.sleep(1)
+                wait_count = wait_count + 1
+                attr_value = await self.read_expect_success(
+                    endpoint=endpoint,
+                    attribute=attributes.OperationalState)
+            count = sub_handler.attribute_report_counts[attributes.CountdownTime]
+            asserts.assert_less_equal(count, 5, "Received more than 5 reports for CountdownTime")
+            asserts.assert_greater(count, 0, "Did not receive any reports for CountdownTime")
+        else:
+            self.skip_step(5)
+
+        sub_handler.reset()
+        self.step(6)
+        if self.pics_guard(self.check_pics(f"{self.test_info.pics_code}.S.M.ST_RUNNING")):
+            self.send_manual_or_pipe_command(name="OperationalStateChange",
+                                             device=self.device,
+                                             operation="Start")
+            time.sleep(1)
+            await self.read_and_expect_value(endpoint=endpoint,
+                                             attribute=attributes.OperationalState,
+                                             expected_value=cluster.Enums.OperationalStateEnum.kRunning)
+            count = sub_handler.attribute_report_counts[attributes.CountdownTime]
+            asserts.assert_greater(count, 0, "Did not receive any reports for CountdownTime")
+        else:
+            self.skip_step(6)
+
+        self.step(7)
+        await self.read_and_expect_value(endpoint=endpoint,
+                                         attribute=attributes.OperationalState,
+                                         expected_value=cluster.Enums.OperationalStateEnum.kRunning)
+
+        sub_handler.reset()
+        self.step(8)
+        if self.pics_guard(self.check_pics(f"{self.test_info.pics_code}.S.M.ST_PAUSED")):
+            self.send_manual_or_pipe_command(name="OperationalStateChange",
+                                             device=self.device,
+                                             operation="Pause")
+            time.sleep(1)
+            count = sub_handler.attribute_report_counts[attributes.CountdownTime]
+            asserts.assert_greater(count, 0, "Did not receive any reports for CountdownTime")
+        else:
+            self.skip_step(8)