text: /*
   *
   *    Copyright (c) 2021 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.
   */
  
  /**
   *    @file
   *      This file implements unit tests for CHIP Interaction Model Command Interaction
   *
   */
  
  #include "app-common/zap-generated/ids/Attributes.h"
  #include "app-common/zap-generated/ids/Clusters.h"
  #include "protocols/interaction_model/Constants.h"
  #include <app-common/zap-generated/cluster-objects.h>
  #include <app/AppBuildConfig.h>
  #include <app/CommandHandlerInterface.h>
  #include <app/InteractionModelEngine.h>
  #include <app/tests/AppTestContext.h>
  #include <app/util/attribute-storage.h>
  #include <controller/InvokeInteraction.h>
  #include <controller/ReadInteraction.h>
  #include <lib/support/ErrorStr.h>
  #include <lib/support/UnitTestRegistration.h>
  #include <lib/support/logging/CHIPLogging.h>
  #include <messaging/tests/MessagingContext.h>
  #include <nlunit-test.h>
  
  using TestContext = chip::Test::AppContext;
  
  using namespace chip;
  using namespace chip::app;
  using namespace chip::app::Clusters;
  
  namespace {
  constexpr EndpointId kTestEndpointId = 1;
  
  enum ResponseDirective
  {
      kSendDataResponse,
      kSendSuccessStatusCode,
      kSendError,
      kSendSuccessStatusCodeWithClusterStatus,
      kSendErrorWithClusterStatus,
  };
  
  ResponseDirective responseDirective;
  
  class TestClusterCommandHandler : public chip::app::CommandHandlerInterface
  {
  public:
      TestClusterCommandHandler() : chip::app::CommandHandlerInterface(Optional<EndpointId>::Missing(), TestCluster::Id)
      {
          chip::app::InteractionModelEngine::GetInstance()->RegisterCommandHandler(this);
      }
  
      ~TestClusterCommandHandler() { chip::app::InteractionModelEngine::GetInstance()->UnregisterCommandHandler(this); }
  
      void OverrideAcceptedCommands() { mOverrideAcceptedCommands = true; }
      void ClaimNoCommands() { mClaimNoCommands = true; }
  
  private:
      void InvokeCommand(chip::app::CommandHandlerInterface::HandlerContext & handlerContext) final;
      CHIP_ERROR EnumerateAcceptedCommands(const ConcreteClusterPath & cluster, CommandIdCallback callback, void * context) final;
  
      bool mOverrideAcceptedCommands = false;
      bool mClaimNoCommands          = false;
  };
  
  void TestClusterCommandHandler::InvokeCommand(chip::app::CommandHandlerInterface::HandlerContext & handlerContext)
  {
      HandleCommand<TestCluster::Commands::TestSimpleArgumentRequest::DecodableType>(
          handlerContext, [](chip::app::CommandHandlerInterface::HandlerContext & ctx, const auto & requestPayload) {
              if (responseDirective == kSendDataResponse)
              {
                  TestCluster::Commands::TestStructArrayArgumentResponse::Type dataResponse;
                  TestCluster::Structs::NestedStructList::Type nestedStructList[4];
  
                  uint8_t i = 0;
                  for (auto & item : nestedStructList)
                  {
                      item.a   = i;
                      item.b   = false;
                      item.c.a = i;
                      item.c.b = true;
                      i++;
                  }
  
                  dataResponse.arg1 = nestedStructList;
                  dataResponse.arg6 = true;
  
                  ctx.mCommandHandler.AddResponse(ctx.mRequestPath, dataResponse);
              }
  
              return CHIP_NO_ERROR;
          });
  }
  
  CHIP_ERROR TestClusterCommandHandler::EnumerateAcceptedCommands(const ConcreteClusterPath & cluster,
                                                                  CommandHandlerInterface::CommandIdCallback callback, void * context)
  {
      if (!mOverrideAcceptedCommands)
      {
          return CHIP_ERROR_NOT_IMPLEMENTED;
      }
  
      if (mClaimNoCommands)
      {
          return CHIP_NO_ERROR;
      }
  
      // We just have one command id.
      callback(TestCluster::Commands::TestSimpleArgumentRequest::Id, context);
      return CHIP_NO_ERROR;
  }
  
  } // namespace
  
  namespace {
  
  class TestCommandInteraction
  {
  public:
      TestCommandInteraction() {}
      static void TestNoHandler(nlTestSuite * apSuite, void * apContext);
      static void TestDataResponse(nlTestSuite * apSuite, void * apContext);
      static void TestDataResponseNoCommand1(nlTestSuite * apSuite, void * apContext);
      static void TestDataResponseNoCommand2(nlTestSuite * apSuite, void * apContext);
      static void TestDataResponseNoCommand3(nlTestSuite * apSuite, void * apContext);
      static void TestDataResponseHandlerOverride1(nlTestSuite * apSuite, void * apContext);
      static void TestDataResponseHandlerOverride2(nlTestSuite * apSuite, void * apContext);
  
  private:
      static void TestDataResponseHelper(nlTestSuite * apSuite, void * apContext, const EmberAfEndpointType * aEndpoint,
                                         bool aExpectSuccess);
  };
  
  // We want to send a TestSimpleArgumentRequest::Type, but get a
  // TestStructArrayArgumentResponse in return, so need to shadow the actual
  // ResponseType that TestSimpleArgumentRequest has.
  struct FakeRequest : public TestCluster::Commands::TestSimpleArgumentRequest::Type
  {
      using ResponseType = TestCluster::Commands::TestStructArrayArgumentResponse::DecodableType;
  };
  
  void TestCommandInteraction::TestNoHandler(nlTestSuite * apSuite, void * apContext)
  {
      TestContext & ctx = *static_cast<TestContext *>(apContext);
      FakeRequest request;
      auto sessionHandle = ctx.GetSessionBobToAlice();
  
      request.arg1 = true;
  
      // Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
      // not safe to do so.
      auto onSuccessCb = [apSuite](const app::ConcreteCommandPath & commandPath, const app::StatusIB & aStatus,
                                   const auto & dataResponse) {
          //
          // We shouldn't be arriving here, since we don't have a command handler installed.
          //
          NL_TEST_ASSERT(apSuite, false);
      };
  
      // Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
      // not safe to do so.
      auto onFailureCb = [apSuite](CHIP_ERROR aError) {
          NL_TEST_ASSERT(apSuite,
                         aError.IsIMStatus() &&
                             app::StatusIB(aError).mStatus == Protocols::InteractionModel::Status::UnsupportedEndpoint);
      };
  
      responseDirective = kSendDataResponse;
  
      ctx.EnableAsyncDispatch();
  
      chip::Controller::InvokeCommandRequest(&ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, request, onSuccessCb,
                                             onFailureCb);
  
      ctx.DrainAndServiceIO();
  
      NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
  }
  
  static const int kDescriptorAttributeArraySize = 254;
  
  // Declare Descriptor cluster attributes
  DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(descriptorAttrs)
  DECLARE_DYNAMIC_ATTRIBUTE(chip::app::Clusters::Descriptor::Attributes::DeviceList::Id, ARRAY, kDescriptorAttributeArraySize,
                            0), /* device list */
      DECLARE_DYNAMIC_ATTRIBUTE(chip::app::Clusters::Descriptor::Attributes::ServerList::Id, ARRAY, kDescriptorAttributeArraySize,
                                0), /* server list */
      DECLARE_DYNAMIC_ATTRIBUTE(chip::app::Clusters::Descriptor::Attributes::ClientList::Id, ARRAY, kDescriptorAttributeArraySize,
                                0), /* client list */
      DECLARE_DYNAMIC_ATTRIBUTE(chip::app::Clusters::Descriptor::Attributes::PartsList::Id, ARRAY, kDescriptorAttributeArraySize,
                                0), /* parts list */
      DECLARE_DYNAMIC_ATTRIBUTE_LIST_END();
  
  DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(testClusterAttrs)
  DECLARE_DYNAMIC_ATTRIBUTE_LIST_END();
  
  constexpr CommandId testClusterCommands1[] = {
      TestCluster::Commands::TestSimpleArgumentRequest::Id,
      kInvalidCommandId,
  };
  DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpointClusters1)
  DECLARE_DYNAMIC_CLUSTER(chip::app::Clusters::TestCluster::Id, testClusterAttrs, testClusterCommands1, nullptr),
      DECLARE_DYNAMIC_CLUSTER(chip::app::Clusters::Descriptor::Id, descriptorAttrs, nullptr, nullptr),
      DECLARE_DYNAMIC_CLUSTER_LIST_END;
  
  DECLARE_DYNAMIC_ENDPOINT(testEndpoint1, testEndpointClusters1);
  
  constexpr CommandId testClusterCommands2[] = {
      kInvalidCommandId,
  };
  DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpointClusters2)
  DECLARE_DYNAMIC_CLUSTER(chip::app::Clusters::TestCluster::Id, testClusterAttrs, testClusterCommands2, nullptr),
      DECLARE_DYNAMIC_CLUSTER(chip::app::Clusters::Descriptor::Id, descriptorAttrs, nullptr, nullptr),
      DECLARE_DYNAMIC_CLUSTER_LIST_END;
  
  DECLARE_DYNAMIC_ENDPOINT(testEndpoint2, testEndpointClusters2);
  
  DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(testEndpointClusters3)
  DECLARE_DYNAMIC_CLUSTER(chip::app::Clusters::TestCluster::Id, testClusterAttrs, nullptr, nullptr),
      DECLARE_DYNAMIC_CLUSTER(chip::app::Clusters::Descriptor::Id, descriptorAttrs, nullptr, nullptr),
      DECLARE_DYNAMIC_CLUSTER_LIST_END;
  
  DECLARE_DYNAMIC_ENDPOINT(testEndpoint3, testEndpointClusters3);
  
  void TestCommandInteraction::TestDataResponseHelper(nlTestSuite * apSuite, void * apContext, const EmberAfEndpointType * aEndpoint,
                                                      bool aExpectSuccess)
  {
      TestContext & ctx = *static_cast<TestContext *>(apContext);
      FakeRequest request;
      auto sessionHandle = ctx.GetSessionBobToAlice();
  
      bool onSuccessWasCalled = false;
      bool onFailureWasCalled = false;
  
      request.arg1 = true;
  
      //
      // Register descriptors for this endpoint since they are needed
      // at command validation time to ensure the command actually exists on that
      // endpoint.
      //
      // All our endpoints have the same number of clusters, so just pick one.
      //
      DataVersion dataVersionStorage[ArraySize(testEndpointClusters1)];
      emberAfSetDynamicEndpoint(0, kTestEndpointId, aEndpoint, 0, 0, Span<DataVersion>(dataVersionStorage));
  
      // Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
      // not safe to do so.
      auto onSuccessCb = [apSuite, &onSuccessWasCalled](const app::ConcreteCommandPath & commandPath, const app::StatusIB & aStatus,
                                                        const auto & dataResponse) {
          uint8_t i = 0;
          auto iter = dataResponse.arg1.begin();
          while (iter.Next())
          {
              auto & item = iter.GetValue();
  
              NL_TEST_ASSERT(apSuite, item.a == i);
              NL_TEST_ASSERT(apSuite, item.b == false);
              NL_TEST_ASSERT(apSuite, item.c.a == i);
              NL_TEST_ASSERT(apSuite, item.c.b == true);
              i++;
          }
  
          NL_TEST_ASSERT(apSuite, iter.GetStatus() == CHIP_NO_ERROR);
          NL_TEST_ASSERT(apSuite, dataResponse.arg6 == true);
  
          onSuccessWasCalled = true;
      };
  
      // Passing of stack variables by reference is only safe because of synchronous completion of the interaction. Otherwise, it's
      // not safe to do so.
      auto onFailureCb = [&onFailureWasCalled](CHIP_ERROR aError) { onFailureWasCalled = true; };
  
      responseDirective = kSendDataResponse;
  
      chip::Controller::InvokeCommandRequest(&ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, request, onSuccessCb,
                                             onFailureCb);
  
      ctx.DrainAndServiceIO();
  
      NL_TEST_ASSERT(apSuite, onSuccessWasCalled == aExpectSuccess && onFailureWasCalled != aExpectSuccess);
      NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
  
      onSuccessWasCalled = false;
      onFailureWasCalled = false;
  
      auto readSuccessCb = [apSuite, &onSuccessWasCalled, aExpectSuccess](const ConcreteDataAttributePath &,
                                                                          const DataModel::DecodableList<CommandId> & commandList) {
          auto count = 0;
          auto iter  = commandList.begin();
          while (iter.Next())
          {
              // We only expect 0 or 1 command ids here.
              NL_TEST_ASSERT(apSuite, count == 0);
              NL_TEST_ASSERT(apSuite, iter.GetValue() == TestCluster::Commands::TestSimpleArgumentRequest::Id);
              ++count;
          }
          NL_TEST_ASSERT(apSuite, iter.GetStatus() == CHIP_NO_ERROR);
          if (aExpectSuccess)
          {
              NL_TEST_ASSERT(apSuite, count == 1);
          }
          else
          {
              NL_TEST_ASSERT(apSuite, count == 0);
          }
          onSuccessWasCalled = true;
      };
      auto readFailureCb = [&onFailureWasCalled](const ConcreteDataAttributePath *, CHIP_ERROR aError) { onFailureWasCalled = true; };
  
      chip::Controller::ReadAttribute<TestCluster::Attributes::AcceptedCommandList::TypeInfo>(
          &ctx.GetExchangeManager(), sessionHandle, kTestEndpointId, readSuccessCb, readFailureCb);
  
      ctx.DrainAndServiceIO();
  
      NL_TEST_ASSERT(apSuite, onSuccessWasCalled && !onFailureWasCalled);
      NL_TEST_ASSERT(apSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
  
      emberAfClearDynamicEndpoint(0);
  }
  
  void TestCommandInteraction::TestDataResponse(nlTestSuite * apSuite, void * apContext)
  {
      TestClusterCommandHandler commandHandler;
      TestDataResponseHelper(apSuite, apContext, &testEndpoint1, true);
  }
  
  void TestCommandInteraction::TestDataResponseNoCommand1(nlTestSuite * apSuite, void * apContext)
  {
      // Check what happens if we don't claim our command id is supported, by
      // overriding the acceptedCommandList with an empty list.
      TestClusterCommandHandler commandHandler;
      commandHandler.OverrideAcceptedCommands();
      commandHandler.ClaimNoCommands();
      TestDataResponseHelper(apSuite, apContext, &testEndpoint1, false);
  }
  
  void TestCommandInteraction::TestDataResponseNoCommand2(nlTestSuite * apSuite, void * apContext)
  {
      // Check what happens if we don't claim our command id is supported, by
      // having an acceptedCommandList that ends immediately.
      TestClusterCommandHandler commandHandler;
      TestDataResponseHelper(apSuite, apContext, &testEndpoint2, false);
  }
  
  void TestCommandInteraction::TestDataResponseNoCommand3(nlTestSuite * apSuite, void * apContext)
  {
      // Check what happens if we don't claim our command id is supported, by
      // having an acceptedCommandList that is null.
      TestClusterCommandHandler commandHandler;
      TestDataResponseHelper(apSuite, apContext, &testEndpoint3, false);
  }
  
  void TestCommandInteraction::TestDataResponseHandlerOverride1(nlTestSuite * apSuite, void * apContext)
  {
      TestClusterCommandHandler commandHandler;
      commandHandler.OverrideAcceptedCommands();
      TestDataResponseHelper(apSuite, apContext, &testEndpoint2, true);
  }
  
  void TestCommandInteraction::TestDataResponseHandlerOverride2(nlTestSuite * apSuite, void * apContext)
  {
      TestClusterCommandHandler commandHandler;
      commandHandler.OverrideAcceptedCommands();
      TestDataResponseHelper(apSuite, apContext, &testEndpoint3, true);
  }
  
  // clang-format off
  const nlTest sTests[] =
  {
      NL_TEST_DEF("TestNoHandler", TestCommandInteraction::TestNoHandler),
      NL_TEST_DEF("TestDataResponse", TestCommandInteraction::TestDataResponse),
      NL_TEST_DEF("TestDataResponseNoCommand1", TestCommandInteraction::TestDataResponseNoCommand1),
      NL_TEST_DEF("TestDataResponseNoCommand2", TestCommandInteraction::TestDataResponseNoCommand2),
      NL_TEST_DEF("TestDataResponseNoCommand3", TestCommandInteraction::TestDataResponseNoCommand3),
      NL_TEST_DEF("TestDataResponseHandlerOverride1", TestCommandInteraction::TestDataResponseHandlerOverride1),
      NL_TEST_DEF("TestDataResponseHandlerOverride2", TestCommandInteraction::TestDataResponseHandlerOverride2),
      NL_TEST_SENTINEL()
  };
  
  // clang-format on
  
  // clang-format off
  nlTestSuite sSuite =
  {
      "TestCommands",
      &sTests[0],
      TestContext::InitializeAsync,
      TestContext::Finalize
  };
  // clang-format on
  
  } // namespace
  
  int TestCommandInteractionTest()
  {
      TestContext gContext;
      nlTestRunner(&sSuite, &gContext);
      return (nlTestRunnerStats(&sSuite));
  }
  
  CHIP_REGISTER_TEST_SUITE(TestCommandInteractionTest)
  