// Copyright 2025 Sony Group Corporation.
//
// 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.

#include <algorithm>
#include <array>
#include <cstddef>
#include <chrono>
#include <future>
#include <map>
#include <memory>
#include <utility>
#include <vector>

#include "gtest/gtest.h"

#include "rcl/allocator.h"
#include "rcl/time.h"
#include "rcl/types.h"

#include "rcl_action/default_qos.h"
#include "rcl_action/names.h"
#include "rcl_action/wait.h"

#include "rclcpp/node.hpp"
#include "rclcpp/executors.hpp"

#include "rclcpp_action/client_goal_handle.hpp"
#include "rclcpp_action/create_generic_client.hpp"
#include "rclcpp_action/generic_client.hpp"
#include "rclcpp_action/qos.hpp"
#include "rclcpp_action/server.hpp"

#include "test_msgs/action/fibonacci.hpp"
#include "test_msgs/msg/empty.hpp"

#include "mocking_utils/patch.hpp"

using namespace std::chrono_literals;

// Refer to the cases in test_client.cpp to implement tests for GenericClient

const auto WAIT_FOR_SERVER_TIMEOUT = 10s;

class TestGenericClient : public ::testing::Test
{
protected:
  using ActionType = test_msgs::action::Fibonacci;
  using ActionGoal = ActionType::Goal;
  using ActionResult = ActionType::Result;
  using ActionGoalHandle = rclcpp_action::ClientGoalHandle<ActionType>;
  using ActionGoalRequestService = ActionType::Impl::SendGoalService;
  using ActionGoalRequest = ActionGoalRequestService::Request;
  using ActionGoalResponse = ActionGoalRequestService::Response;
  using ActionGoalResultService = ActionType::Impl::GetResultService;
  using ActionGoalResultRequest = ActionGoalResultService::Request;
  using ActionGoalResultResponse = ActionGoalResultService::Response;
  using ActionCancelGoalService = ActionType::Impl::CancelGoalService;
  using ActionCancelGoalRequest = ActionType::Impl::CancelGoalService::Request;
  using ActionCancelGoalResponse = ActionType::Impl::CancelGoalService::Response;
  using ActionStatusMessage = ActionType::Impl::GoalStatusMessage;
  using ActionFeedbackMessage = ActionType::Impl::FeedbackMessage;
  using ActionFeedback = ActionType::Feedback;

  static void SetUpTestCase()
  {
    rclcpp::init(0, nullptr);
  }

  void SetUpServer()
  {
    rcl_allocator_t allocator = rcl_get_default_allocator();

    server_node = std::make_shared<rclcpp::Node>(server_node_name, namespace_name);

    char * goal_service_name = nullptr;
    rcl_ret_t ret = rcl_action_get_goal_service_name(
      action_name, allocator, &goal_service_name);
    ASSERT_EQ(RCL_RET_OK, ret);
    goal_service = server_node->create_service<ActionGoalRequestService>(
      goal_service_name,
      [this](
        const ActionGoalRequest::SharedPtr request,
        ActionGoalResponse::SharedPtr response)
      {
        response->stamp = clock.now();
        response->accepted = (request->goal.order >= 0);
        if (response->accepted) {
          goals[request->goal_id.uuid] = {request, response};
        }
      });
    ASSERT_TRUE(goal_service != nullptr);
    allocator.deallocate(goal_service_name, allocator.state);

    char * result_service_name = nullptr;
    ret = rcl_action_get_result_service_name(
      action_name, allocator, &result_service_name);
    ASSERT_EQ(RCL_RET_OK, ret);
    result_service = server_node->create_service<ActionGoalResultService>(
      result_service_name,
      [this](
        const ActionGoalResultRequest::SharedPtr request,
        ActionGoalResultResponse::SharedPtr response)
      {
        if (goals.count(request->goal_id.uuid) == 1) {
          auto goal_request = goals[request->goal_id.uuid].first;
          auto goal_response = goals[request->goal_id.uuid].second;
          ActionStatusMessage status_message;
          rclcpp_action::GoalStatus goal_status;
          goal_status.goal_info.goal_id.uuid = goal_request->goal_id.uuid;
          goal_status.goal_info.stamp = goal_response->stamp;
          goal_status.status = rclcpp_action::GoalStatus::STATUS_EXECUTING;
          status_message.status_list.push_back(goal_status);
          status_publisher->publish(status_message);
          client_executor.spin_once();
          while (pending_handle_goal_) {
            // wait until allowed to proceed
            std::this_thread::sleep_for(200ms);
          }
          ActionFeedbackMessage feedback_message;
          feedback_message.goal_id.uuid = goal_request->goal_id.uuid;
          feedback_message.feedback.sequence.push_back(0);
          feedback_publisher->publish(feedback_message);
          client_executor.spin_once();
          if (goal_request->goal.order > 0) {
            feedback_message.feedback.sequence.push_back(1);
            feedback_publisher->publish(feedback_message);
            client_executor.spin_once();
            for (size_t i = 1; i < static_cast<size_t>(goal_request->goal.order); ++i) {
              feedback_message.feedback.sequence.push_back(
                feedback_message.feedback.sequence[i] +
                feedback_message.feedback.sequence[i - 1]);
              feedback_publisher->publish(feedback_message);
              client_executor.spin_once();
            }
          }
          goal_status.status = rclcpp_action::GoalStatus::STATUS_SUCCEEDED;
          status_message.status_list[0] = goal_status;
          status_publisher->publish(status_message);
          client_executor.spin_once();
          response->result.sequence = feedback_message.feedback.sequence;
          response->status = rclcpp_action::GoalStatus::STATUS_SUCCEEDED;
          goals.erase(request->goal_id.uuid);
        } else {
          response->status = rclcpp_action::GoalStatus::STATUS_UNKNOWN;
        }
      });
    ASSERT_TRUE(result_service != nullptr);
    allocator.deallocate(result_service_name, allocator.state);

    char * cancel_service_name = nullptr;
    ret = rcl_action_get_cancel_service_name(
      action_name, allocator, &cancel_service_name);
    ASSERT_EQ(RCL_RET_OK, ret);
    cancel_service = server_node->create_service<ActionCancelGoalService>(
      cancel_service_name,
      [this](
        const ActionCancelGoalRequest::SharedPtr request,
        ActionCancelGoalResponse::SharedPtr response)
      {
        rclcpp_action::GoalUUID zero_uuid;
        std::fill(zero_uuid.begin(), zero_uuid.end(), 0u);
        const rclcpp::Time cancel_stamp = request->goal_info.stamp;
        bool cancel_all = (
          request->goal_info.goal_id.uuid == zero_uuid &&
          cancel_stamp == zero_stamp);
        ActionStatusMessage status_message;
        auto it = goals.begin();
        while (it != goals.end()) {
          auto goal_request = it->second.first;
          auto goal_response = it->second.second;
          const rclcpp::Time goal_stamp = goal_response->stamp;
          bool cancel_this = (
            request->goal_info.goal_id.uuid == goal_request->goal_id.uuid ||
            cancel_stamp > goal_stamp);
          if (cancel_all || cancel_this) {
            rclcpp_action::GoalStatus goal_status;
            goal_status.goal_info.goal_id.uuid = goal_request->goal_id.uuid;
            goal_status.goal_info.stamp = goal_response->stamp;
            goal_status.status = rclcpp_action::GoalStatus::STATUS_CANCELED;
            status_message.status_list.push_back(goal_status);
            response->goals_canceling.push_back(goal_status.goal_info);
            it = goals.erase(it);
          } else {
            ++it;
          }
        }
        status_publisher->publish(status_message);
        client_executor.spin_once();
      });
    ASSERT_TRUE(cancel_service != nullptr);
    allocator.deallocate(cancel_service_name, allocator.state);

    char * feedback_topic_name = nullptr;
    ret = rcl_action_get_feedback_topic_name(
      action_name, allocator, &feedback_topic_name);
    ASSERT_EQ(RCL_RET_OK, ret);
    feedback_publisher =
      server_node->create_publisher<ActionFeedbackMessage>(feedback_topic_name, 10);
    ASSERT_TRUE(feedback_publisher != nullptr);
    allocator.deallocate(feedback_topic_name, allocator.state);

    char * status_topic_name = nullptr;
    ret = rcl_action_get_status_topic_name(
      action_name, allocator, &status_topic_name);
    ASSERT_EQ(RCL_RET_OK, ret);
    status_publisher = server_node->create_publisher<ActionStatusMessage>(
      status_topic_name, rclcpp_action::DefaultActionStatusQoS());
    ASSERT_TRUE(status_publisher != nullptr);
    allocator.deallocate(status_topic_name, allocator.state);
    server_executor.add_node(server_node);
  }

  void SetUp() override
  {
    client_node = std::make_shared<rclcpp::Node>(client_node_name, namespace_name);
    client_executor.add_node(client_node);

    ASSERT_EQ(RCL_RET_OK, rcl_enable_ros_time_override(clock.get_clock_handle()));
    ASSERT_EQ(RCL_RET_OK, rcl_set_ros_time_override(clock.get_clock_handle(), RCL_S_TO_NS(1)));

    // Check if subscription content filter is supported. This affects whether some tests need
    // to be executed.
    {
      auto subscription = client_node->create_subscription<test_msgs::msg::Empty>(
        "test_topic", 10, [](const test_msgs::msg::Empty::SharedPtr) {});
      subscription_content_filter_supported_ = subscription->is_cft_supported();
    }
  }

  static void TearDownTestCase()
  {
    rclcpp::shutdown();
  }

  void TearDownServer()
  {
    status_publisher.reset();
    feedback_publisher.reset();
    cancel_service.reset();
    result_service.reset();
    goal_service.reset();
    server_node.reset();
  }

  void TearDown() override
  {
    client_node.reset();
  }

  template<typename FutureT>
  void dual_spin_until_future_complete(std::shared_future<FutureT> & future)
  {
    std::future_status status;
    do {
      server_executor.spin_some();
      client_executor.spin_some();
      status = future.wait_for(std::chrono::seconds(0));
    } while (std::future_status::ready != status);
  }

  void enable_pending_handling_goal()
  {
    pending_handle_goal_ = true;
  }

  void disable_pending_handling_goal()
  {
    pending_handle_goal_ = false;
  }

  rclcpp::Clock clock{RCL_ROS_TIME};
  const rclcpp::Time zero_stamp{0, 0, RCL_ROS_TIME};

  rclcpp::Node::SharedPtr server_node;
  rclcpp::executors::SingleThreadedExecutor server_executor;
  rclcpp::Node::SharedPtr client_node;
  rclcpp::executors::SingleThreadedExecutor client_executor;
  const char * const server_node_name{"fibonacci_action_test_server"};
  const char * const client_node_name{"fibonacci_action_test_client"};
  const char * const namespace_name{"/rclcpp_action/test/client"};
  const char * const action_name{"fibonacci_test"};

  std::map<
    rclcpp_action::GoalUUID,
    std::pair<
      typename ActionGoalRequest::SharedPtr,
      typename ActionGoalResponse::SharedPtr>> goals;
  typename rclcpp::Service<ActionGoalRequestService>::SharedPtr goal_service;
  typename rclcpp::Service<ActionGoalResultService>::SharedPtr result_service;
  typename rclcpp::Service<ActionCancelGoalService>::SharedPtr cancel_service;
  typename rclcpp::Publisher<ActionFeedbackMessage>::SharedPtr feedback_publisher;
  typename rclcpp::Publisher<ActionStatusMessage>::SharedPtr status_publisher;
  std::atomic_bool pending_handle_goal_{false};
  bool subscription_content_filter_supported_{false};
};

TEST_F(TestGenericClient, construction_with_free_function) {
  {
    ASSERT_NO_THROW({
      auto client = rclcpp_action::create_generic_client(
        client_node->get_node_base_interface(),
        client_node->get_node_graph_interface(),
        client_node->get_node_logging_interface(),
        client_node->get_node_waitables_interface(),
        "fibonacci",
        "test_msgs/action/Fibonacci",
        nullptr);
    });
  }
  {
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_client_init, RCL_RET_ERROR);
    ASSERT_THROW(
    {
      auto client = rclcpp_action::create_generic_client(
        client_node->get_node_base_interface(),
        client_node->get_node_graph_interface(),
        client_node->get_node_logging_interface(),
        client_node->get_node_waitables_interface(),
        action_name,
        "test_msgs/action/Fibonacci",
        nullptr);
    }, rclcpp::exceptions::RCLError);
  }
  {
    ASSERT_NO_THROW({
      auto client = rclcpp_action::create_generic_client(
        client_node,
        action_name,
        "test_msgs/action/Fibonacci",
        nullptr);
    });
  }
  {
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_client_init, RCL_RET_ERROR);
    ASSERT_THROW(
    {
      auto client = rclcpp_action::create_generic_client(
        client_node,
        action_name,
        "test_msgs/action/Fibonacci",
        nullptr);
    }, rclcpp::exceptions::RCLError);
  }
}

TEST_F(TestGenericClient, construction_and_destruction_after_node)
{
  ASSERT_NO_THROW(
  {
    auto action_client = rclcpp_action::create_generic_client(
      client_node->get_node_base_interface(),
      client_node->get_node_graph_interface(),
      client_node->get_node_logging_interface(),
      client_node->get_node_waitables_interface(),
      action_name,
      "test_msgs/action/Fibonacci",
      nullptr);
    client_node.reset();
  });
}

TEST_F(TestGenericClient, construction_and_destruction_callback_group)
{
  auto group = client_node->create_callback_group(
    rclcpp::CallbackGroupType::MutuallyExclusive);
  const rcl_action_client_options_t & options = rcl_action_client_get_default_options();
  ASSERT_NO_THROW(
    rclcpp_action::create_generic_client(
      client_node->get_node_base_interface(),
      client_node->get_node_graph_interface(),
      client_node->get_node_logging_interface(),
      client_node->get_node_waitables_interface(),
      action_name,
      "test_msgs/action/Fibonacci",
      group,
      options).reset());
}

TEST_F(TestGenericClient, wait_for_action_server)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  EXPECT_FALSE(action_generic_client->wait_for_action_server(0ms));
  EXPECT_FALSE(action_generic_client->wait_for_action_server(10ms));
  SetUpServer();
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));
  TearDownServer();

  client_node.reset();  // Drop node before action client
  EXPECT_THROW(action_generic_client->wait_for_action_server(0ms),
    rclcpp::exceptions::InvalidNodeError);
}

TEST_F(TestGenericClient, wait_for_action_server_rcl_errors)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  SetUpServer();
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));
  {
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_server_is_available, RCL_RET_NODE_INVALID);
    EXPECT_THROW(action_generic_client->action_server_is_ready(), rclcpp::exceptions::RCLError);

    auto mock_context_is_valid = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_context_is_valid, false);
    EXPECT_FALSE(action_generic_client->action_server_is_ready());
  }
  {
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_server_is_available, RCL_RET_ERROR);
    EXPECT_THROW(action_generic_client->action_server_is_ready(), rclcpp::exceptions::RCLError);
  }
  TearDownServer();
}

TEST_F(TestGenericClient, is_ready) {
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  rcl_wait_set_t wait_set = rcl_get_zero_initialized_wait_set();
  rcutils_allocator_t allocator = rcutils_get_default_allocator();
  auto rcl_context = client_node->get_node_base_interface()->get_context()->get_rcl_context().get();
  ASSERT_EQ(
    RCL_RET_OK,
    rcl_wait_set_init(&wait_set, 10, 10, 10, 10, 10, 10, rcl_context, allocator));
  ASSERT_NO_THROW(action_generic_client->add_to_wait_set(wait_set));
  EXPECT_TRUE(action_generic_client->is_ready(wait_set));

  {
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_client_wait_set_get_entities_ready, RCL_RET_ERROR);
    EXPECT_THROW(action_generic_client->is_ready(wait_set), rclcpp::exceptions::RCLError);
  }
  client_node.reset();  // Drop node before action client
}

class TestGenericClientAgainstServer : public TestGenericClient
{
protected:
  void SetUp() override
  {
    disable_pending_handling_goal();
    SetUpServer();
    TestGenericClient::SetUp();
  }

  void TearDown() override
  {
    TestGenericClient::TearDown();
    TearDownServer();
  }
};

TEST_F(TestGenericClientAgainstServer, async_send_goal_no_callbacks)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal bad_goal;
  bad_goal.order = -5;
  auto future_goal_handle =
    action_generic_client->async_send_goal(&bad_goal, sizeof(bad_goal));
  dual_spin_until_future_complete(future_goal_handle);
  EXPECT_EQ(nullptr, future_goal_handle.get().get());

  ActionGoal good_goal;
  good_goal.order = 5;
  future_goal_handle =
    action_generic_client->async_send_goal(&good_goal, sizeof(good_goal));
  dual_spin_until_future_complete(future_goal_handle);
  auto goal_handle = future_goal_handle.get();
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());
  EXPECT_FALSE(goal_handle->is_feedback_aware());
  EXPECT_FALSE(goal_handle->is_result_aware());
}

TEST_F(TestGenericClientAgainstServer, async_send_goal_request_no_callbacks)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoalRequest bad_goal_request;
  bad_goal_request.goal.order = -5;
  auto future_goal_handle =
    action_generic_client->async_send_goal(&bad_goal_request);
  dual_spin_until_future_complete(future_goal_handle);
  EXPECT_EQ(nullptr, future_goal_handle.get().get());

  ActionGoalRequest good_goal_request;
  good_goal_request.goal.order = 5;
  future_goal_handle =
    action_generic_client->async_send_goal(&good_goal_request);
  dual_spin_until_future_complete(future_goal_handle);
  auto goal_handle = future_goal_handle.get();
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());
  EXPECT_FALSE(goal_handle->is_feedback_aware());
  EXPECT_FALSE(goal_handle->is_result_aware());
}

TEST_F(TestGenericClientAgainstServer, bad_goal_handles)
{
  auto action_generic_client0 = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client0->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 0;
  auto future_goal_handle = action_generic_client0->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle);
  auto goal_handle = future_goal_handle.get();

  auto action_generic_client1 = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  using rclcpp_action::exceptions::UnknownGoalHandleError;
  EXPECT_THROW(action_generic_client1->async_get_result(goal_handle), UnknownGoalHandleError);
  EXPECT_THROW(action_generic_client1->async_cancel_goal(goal_handle), UnknownGoalHandleError);
}

TEST_F(TestGenericClientAgainstServer, async_send_goal_no_callbacks_wait_for_result)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 5;
  auto future_goal_handle = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle);
  auto goal_handle = future_goal_handle.get();
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());
  EXPECT_FALSE(goal_handle->is_feedback_aware());
  EXPECT_FALSE(goal_handle->is_result_aware());
  auto future_result = action_generic_client->async_get_result(goal_handle);
  EXPECT_TRUE(goal_handle->is_result_aware());
  dual_spin_until_future_complete(future_result);
  auto wrapped_result = future_result.get();
  const ActionResult * result = static_cast<const ActionResult *>(wrapped_result.result);
  EXPECT_EQ(wrapped_result.code, rclcpp_action::GenericClientGoalHandle::ResultCode::SUCCEEDED);
  EXPECT_EQ(6ul, result->sequence.size());
  EXPECT_EQ(0, result->sequence[0]);
  EXPECT_EQ(1, result->sequence[1]);
  EXPECT_EQ(5, result->sequence[5]);
}

TEST_F(TestGenericClientAgainstServer, async_send_goal_no_callbacks_then_invalidate)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 5;
  auto future_goal_handle = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle);
  auto goal_handle = future_goal_handle.get();
  ASSERT_NE(nullptr, goal_handle);
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());
  auto future_result = action_generic_client->async_get_result(goal_handle);
  EXPECT_TRUE(goal_handle->is_result_aware());

  action_generic_client.reset();  // Ensure goal handle is invalidated once client goes out of scope

  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_UNKNOWN, goal_handle->get_status());
  using rclcpp_action::exceptions::UnawareGoalHandleError;
  EXPECT_THROW(future_result.get(), UnawareGoalHandleError);
}

TEST_F(TestGenericClientAgainstServer, async_send_goal_with_goal_response_callback_wait_for_result)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  bool goal_response_received = false;
  auto send_goal_ops = rclcpp_action::GenericClient::SendGoalOptions();
  send_goal_ops.goal_response_callback =
    [&goal_response_received](
    typename rclcpp_action::GenericClientGoalHandle::SharedPtr goal_handle)
    {
      if (goal_handle) {
        goal_response_received = true;
      }
    };

  {
    ActionGoal bad_goal;
    bad_goal.order = -1;
    auto future_goal_handle =
      action_generic_client->async_send_goal(&bad_goal, sizeof(bad_goal), send_goal_ops);
    dual_spin_until_future_complete(future_goal_handle);
    auto goal_handle = future_goal_handle.get();
    EXPECT_FALSE(goal_response_received);
    EXPECT_EQ(nullptr, goal_handle);
  }

  {
    ActionGoal goal;
    goal.order = 4;
    auto future_goal_handle =
      action_generic_client->async_send_goal(&goal, sizeof(goal), send_goal_ops);
    dual_spin_until_future_complete(future_goal_handle);
    auto goal_handle = future_goal_handle.get();
    EXPECT_TRUE(goal_response_received);
    EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());
    EXPECT_FALSE(goal_handle->is_feedback_aware());
    EXPECT_FALSE(goal_handle->is_result_aware());
    auto future_result = action_generic_client->async_get_result(goal_handle);
    EXPECT_TRUE(goal_handle->is_result_aware());
    dual_spin_until_future_complete(future_result);
    auto wrapped_result = future_result.get();
    const ActionResult * result = (const ActionResult *)(wrapped_result.result);
    ASSERT_EQ(5u, result->sequence.size());
    EXPECT_EQ(3, result->sequence.back());
  }
}

TEST_F(TestGenericClientAgainstServer, async_send_goal_with_feedback_callback_wait_for_result)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 4;
  int feedback_count = 0;
  auto send_goal_ops = rclcpp_action::GenericClient::SendGoalOptions();
  send_goal_ops.feedback_callback = [&feedback_count](
    typename rclcpp_action::GenericClientGoalHandle::SharedPtr goal_handle,
    const void * feedback)
    {
      (void)goal_handle;
      (void)feedback;
      feedback_count++;
    };
  auto future_goal_handle =
    action_generic_client->async_send_goal(&goal, sizeof(goal), send_goal_ops);
  dual_spin_until_future_complete(future_goal_handle);
  auto goal_handle = future_goal_handle.get();
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());
  EXPECT_TRUE(goal_handle->is_feedback_aware());
  EXPECT_FALSE(goal_handle->is_result_aware());
  auto future_result = action_generic_client->async_get_result(goal_handle);
  EXPECT_TRUE(goal_handle->is_result_aware());
  dual_spin_until_future_complete(future_result);
  auto wrapped_result = future_result.get();
  const ActionResult * result = (const ActionResult *)(wrapped_result.result);
  ASSERT_EQ(5u, result->sequence.size());
  EXPECT_EQ(3, result->sequence.back());
  EXPECT_EQ(5, feedback_count);
}

TEST_F(TestGenericClientAgainstServer, async_send_goal_with_result_callback_wait_for_result)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 4;
  bool result_callback_received = false;
  auto send_goal_ops = rclcpp_action::GenericClient::SendGoalOptions();
  send_goal_ops.result_callback =
    [&result_callback_received](
    const rclcpp_action::GenericClientGoalHandle::WrappedResult & wrapped_result)
    {
      const ActionResult * result = (const ActionResult *)(wrapped_result.result);
      if (rclcpp_action::GenericClientGoalHandle::ResultCode::SUCCEEDED == wrapped_result.code &&
        result->sequence.size() == 5u)
      {
        result_callback_received = true;
      }
    };
  auto future_goal_handle =
    action_generic_client->async_send_goal(&goal, sizeof(goal), send_goal_ops);
  dual_spin_until_future_complete(future_goal_handle);
  auto goal_handle = future_goal_handle.get();
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());
  EXPECT_FALSE(goal_handle->is_feedback_aware());
  EXPECT_TRUE(goal_handle->is_result_aware());
  auto future_result = action_generic_client->async_get_result(goal_handle);
  dual_spin_until_future_complete(future_result);
  auto wrapped_result = future_result.get();
  const ActionResult * result = (const ActionResult *)(wrapped_result.result);
  EXPECT_TRUE(result_callback_received);
  ASSERT_EQ(5u, result->sequence.size());
  EXPECT_EQ(3, result->sequence.back());
}

TEST_F(TestGenericClientAgainstServer, async_get_result_with_callback)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 4;
  auto future_goal_handle = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle);
  auto goal_handle = future_goal_handle.get();
  EXPECT_NE(goal_handle, nullptr);
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());
  EXPECT_FALSE(goal_handle->is_feedback_aware());
  EXPECT_FALSE(goal_handle->is_result_aware());
  bool result_callback_received = false;
  auto future_result = action_generic_client->async_get_result(
    goal_handle,
    [&result_callback_received](
      const rclcpp_action::GenericClientGoalHandle::WrappedResult & wrapped_result)
    {
      const ActionResult * result = (const ActionResult *)(wrapped_result.result);
      if (rclcpp_action::GenericClientGoalHandle::ResultCode::SUCCEEDED == wrapped_result.code &&
      result->sequence.size() == 5u)
      {
        result_callback_received = true;
      }
    });
  dual_spin_until_future_complete(future_result);
  auto wrapped_result = future_result.get();
  const ActionResult * result = (const ActionResult *)(wrapped_result.result);
  EXPECT_TRUE(result_callback_received);
  ASSERT_EQ(5u, result->sequence.size());
  EXPECT_EQ(3, result->sequence.back());
}

TEST_F(TestGenericClientAgainstServer, async_cancel_one_goal)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 5;
  auto future_goal_handle = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle);
  auto goal_handle = future_goal_handle.get();
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());

  auto future_cancel = action_generic_client->async_cancel_goal(goal_handle);
  dual_spin_until_future_complete(future_cancel);
  ActionCancelGoalResponse::SharedPtr cancel_response = future_cancel.get();
  EXPECT_EQ(ActionCancelGoalResponse::ERROR_NONE, cancel_response->return_code);
}

TEST_F(TestGenericClientAgainstServer, async_cancel_one_goal_with_callback)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 5;
  auto future_goal_handle = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle);
  auto goal_handle = future_goal_handle.get();
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());

  bool cancel_response_received = false;
  auto future_cancel = action_generic_client->async_cancel_goal(
    goal_handle,
    [&cancel_response_received, goal_handle](
      ActionCancelGoalResponse::SharedPtr response)
    {
      if (
        ActionCancelGoalResponse::ERROR_NONE == response->return_code &&
        1ul == response->goals_canceling.size() &&
        goal_handle->get_goal_id() == response->goals_canceling[0].goal_id.uuid)
      {
        cancel_response_received = true;
      }
    });
  dual_spin_until_future_complete(future_cancel);
  auto cancel_response = future_cancel.get();
  EXPECT_EQ(ActionCancelGoalResponse::ERROR_NONE, cancel_response->return_code);
  ASSERT_EQ(1ul, cancel_response->goals_canceling.size());
  EXPECT_EQ(goal_handle->get_goal_id(), cancel_response->goals_canceling[0].goal_id.uuid);
  EXPECT_TRUE(cancel_response_received);
}

TEST_F(TestGenericClientAgainstServer, async_cancel_all_goals)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 6;
  auto future_goal_handle0 = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle0);
  auto goal_handle0 = future_goal_handle0.get();

  ASSERT_EQ(RCL_RET_OK, rcl_set_ros_time_override(clock.get_clock_handle(), RCL_S_TO_NS(2)));

  goal.order = 8;
  auto future_goal_handle1 = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle1);
  auto goal_handle1 = future_goal_handle1.get();

  if (goal_handle1->get_goal_id() < goal_handle0->get_goal_id()) {
    goal_handle0.swap(goal_handle1);
  }

  ASSERT_EQ(RCL_RET_OK, rcl_set_ros_time_override(clock.get_clock_handle(), RCL_S_TO_NS(3)));

  auto future_cancel_all = action_generic_client->async_cancel_all_goals();
  dual_spin_until_future_complete(future_cancel_all);
  auto cancel_response = future_cancel_all.get();

  EXPECT_EQ(ActionCancelGoalResponse::ERROR_NONE, cancel_response->return_code);
  ASSERT_EQ(2ul, cancel_response->goals_canceling.size());
  EXPECT_EQ(goal_handle0->get_goal_id(), cancel_response->goals_canceling[0].goal_id.uuid);
  EXPECT_EQ(goal_handle1->get_goal_id(), cancel_response->goals_canceling[1].goal_id.uuid);
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_CANCELED, goal_handle0->get_status());
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_CANCELED, goal_handle1->get_status());
}

TEST_F(TestGenericClientAgainstServer, async_cancel_all_goals_with_callback)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 6;
  auto future_goal_handle0 = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle0);
  auto goal_handle0 = future_goal_handle0.get();

  ASSERT_EQ(RCL_RET_OK, rcl_set_ros_time_override(clock.get_clock_handle(), RCL_S_TO_NS(2)));

  goal.order = 8;
  auto future_goal_handle1 = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle1);
  auto goal_handle1 = future_goal_handle1.get();

  if (goal_handle1->get_goal_id() < goal_handle0->get_goal_id()) {
    goal_handle0.swap(goal_handle1);
  }

  ASSERT_EQ(RCL_RET_OK, rcl_set_ros_time_override(clock.get_clock_handle(), RCL_S_TO_NS(3)));

  bool cancel_callback_received = false;
  auto future_cancel_all = action_generic_client->async_cancel_all_goals(
    [&cancel_callback_received, goal_handle0, goal_handle1](
      ActionCancelGoalResponse::SharedPtr response)
    {
      if (
        response &&
        2ul == response->goals_canceling.size() &&
        goal_handle0->get_goal_id() == response->goals_canceling[0].goal_id.uuid &&
        goal_handle1->get_goal_id() == response->goals_canceling[1].goal_id.uuid)
      {
        cancel_callback_received = true;
      }
    });
  dual_spin_until_future_complete(future_cancel_all);
  auto cancel_response = future_cancel_all.get();

  EXPECT_EQ(ActionCancelGoalResponse::ERROR_NONE, cancel_response->return_code);
  EXPECT_TRUE(cancel_callback_received);
  ASSERT_EQ(2ul, cancel_response->goals_canceling.size());
  EXPECT_EQ(goal_handle0->get_goal_id(), cancel_response->goals_canceling[0].goal_id.uuid);
  EXPECT_EQ(goal_handle1->get_goal_id(), cancel_response->goals_canceling[1].goal_id.uuid);
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_CANCELED, goal_handle0->get_status());
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_CANCELED, goal_handle1->get_status());
}

TEST_F(TestGenericClientAgainstServer, async_cancel_some_goals)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 6;
  auto future_goal_handle0 = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle0);
  auto goal_handle0 = future_goal_handle0.get();

  ASSERT_EQ(RCL_RET_OK, rcl_set_ros_time_override(clock.get_clock_handle(), RCL_S_TO_NS(2)));

  goal.order = 8;
  auto future_goal_handle1 = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle1);
  auto goal_handle1 = future_goal_handle1.get();

  ASSERT_EQ(RCL_RET_OK, rcl_set_ros_time_override(clock.get_clock_handle(), RCL_S_TO_NS(3)));

  auto future_cancel_some =
    action_generic_client->async_cancel_goals_before(goal_handle1->get_goal_stamp());
  dual_spin_until_future_complete(future_cancel_some);
  auto cancel_response = future_cancel_some.get();

  EXPECT_EQ(ActionCancelGoalResponse::ERROR_NONE, cancel_response->return_code);
  ASSERT_EQ(1ul, cancel_response->goals_canceling.size());
  EXPECT_EQ(goal_handle0->get_goal_id(), cancel_response->goals_canceling[0].goal_id.uuid);
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_CANCELED, goal_handle0->get_status());
}

TEST_F(TestGenericClientAgainstServer, async_cancel_some_goals_with_callback)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  ActionGoal goal;
  goal.order = 6;
  auto future_goal_handle0 = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle0);
  auto goal_handle0 = future_goal_handle0.get();

  ASSERT_EQ(RCL_RET_OK, rcl_set_ros_time_override(clock.get_clock_handle(), RCL_S_TO_NS(2)));

  goal.order = 8;
  auto future_goal_handle1 = action_generic_client->async_send_goal(&goal, sizeof(goal));
  dual_spin_until_future_complete(future_goal_handle1);
  auto goal_handle1 = future_goal_handle1.get();

  ASSERT_EQ(RCL_RET_OK, rcl_set_ros_time_override(clock.get_clock_handle(), RCL_S_TO_NS(3)));

  bool cancel_callback_received = false;
  auto future_cancel_some = action_generic_client->async_cancel_goals_before(
    goal_handle1->get_goal_stamp(),
    [&cancel_callback_received, goal_handle0](ActionCancelGoalResponse::SharedPtr response)
    {
      if (
        response &&
        1ul == response->goals_canceling.size() &&
        goal_handle0->get_goal_id() == response->goals_canceling[0].goal_id.uuid)
      {
        cancel_callback_received = true;
      }
    });
  dual_spin_until_future_complete(future_cancel_some);
  auto cancel_response = future_cancel_some.get();

  EXPECT_EQ(ActionCancelGoalResponse::ERROR_NONE, cancel_response->return_code);
  EXPECT_TRUE(cancel_callback_received);
  ASSERT_EQ(1ul, cancel_response->goals_canceling.size());
  EXPECT_EQ(goal_handle0->get_goal_id(), cancel_response->goals_canceling[0].goal_id.uuid);
  EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_CANCELED, goal_handle0->get_status());
}

TEST_F(TestGenericClientAgainstServer, deadlock_in_callbacks)
{
  std::atomic<bool> feedback_callback_called = false;
  std::atomic<bool> response_callback_called = false;
  std::atomic<bool> result_callback_called = false;
  std::atomic<bool> no_deadlock = false;

  std::thread tr = std::thread(
    [&]() {
      auto action_generic_client = rclcpp_action::create_generic_client(
        client_node,
        action_name,
        "test_msgs/action/Fibonacci");
      ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

      ActionGoal goal;

      rclcpp_action::GenericClient::SendGoalOptions ops;
      ops.feedback_callback = [&feedback_callback_called](
        typename rclcpp_action::GenericClientGoalHandle::SharedPtr handle, const void *)
      {
        // call functions on the handle that acquire the lock
        handle->get_status();
        handle->is_feedback_aware();
        handle->is_result_aware();

        feedback_callback_called = true;
      };
      ops.goal_response_callback = [&response_callback_called](
        const rclcpp_action::GenericClientGoalHandle::SharedPtr & handle) {
        // call functions on the handle that acquire the lock
        handle->get_status();
        handle->is_feedback_aware();
        handle->is_result_aware();

        response_callback_called = true;
      };
      ops.result_callback = [&result_callback_called](
        const rclcpp_action::GenericClientGoalHandle::WrappedResult & ) {
        result_callback_called = true;
      };

      goal.order = 6;
      auto future_goal_handle = action_generic_client->async_send_goal(&goal, sizeof(goal), ops);
      dual_spin_until_future_complete(future_goal_handle);
      auto goal_handle = future_goal_handle.get();

      ASSERT_TRUE(goal_handle);

      ASSERT_EQ(RCL_RET_OK, rcl_set_ros_time_override(clock.get_clock_handle(), RCL_S_TO_NS(2)));

      auto result_future = action_generic_client->async_get_result(goal_handle);
      dual_spin_until_future_complete(result_future);

      EXPECT_TRUE(result_future.valid());
      auto result = result_future.get();

      no_deadlock = true;
    });

  auto start_time = std::chrono::system_clock::now();

  while (std::chrono::system_clock::now() - start_time < std::chrono::milliseconds(2000) &&
    !no_deadlock)
  {
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
  }

  if (no_deadlock) {
    tr.join();
  } else {
    // In case of a failure, the thread is assumed to be in a deadlock.
    // We detach the thread so we don't block further tests.
    tr.detach();
  }

  EXPECT_TRUE(no_deadlock);
  EXPECT_TRUE(response_callback_called);
  EXPECT_TRUE(result_callback_called);
  EXPECT_TRUE(feedback_callback_called);
}

TEST_F(TestGenericClientAgainstServer, send_rcl_errors)
{
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  auto send_goal_ops = rclcpp_action::GenericClient::SendGoalOptions();
  send_goal_ops.result_callback =
    [](const rclcpp_action::GenericClientGoalHandle::WrappedResult &) {};
  send_goal_ops.feedback_callback = [](
    typename rclcpp_action::GenericClientGoalHandle::SharedPtr, const void *) {};

  {
    ActionGoal goal;
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_send_goal_request, RCL_RET_ERROR);
    EXPECT_THROW(
      action_generic_client->async_send_goal(&goal, sizeof(goal), send_goal_ops),
      rclcpp::exceptions::RCLError);
  }
  {
    ActionGoal goal;
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_send_result_request, RCL_RET_ERROR);
    auto future_goal_handle =
      action_generic_client->async_send_goal(&goal, sizeof(goal), send_goal_ops);
    dual_spin_until_future_complete(future_goal_handle);
    auto goal_handle = future_goal_handle.get();
    EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_UNKNOWN, goal_handle->get_status());
  }
  {
    ActionGoal goal;
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_send_cancel_request, RCL_RET_ERROR);
    auto future_goal_handle =
      action_generic_client->async_send_goal(&goal, sizeof(goal), send_goal_ops);
    dual_spin_until_future_complete(future_goal_handle);
    auto goal_handle = future_goal_handle.get();
    EXPECT_THROW(
      action_generic_client->async_cancel_goals_before(goal_handle->get_goal_stamp()),
      rclcpp::exceptions::RCLError);
  }
}

TEST_F(TestGenericClientAgainstServer, execute_rcl_errors)
{
  // Test that when the feedback message optimization is enabled, the goal's feedback messages
  // are received without any issues.
  auto action_generic_client = rclcpp_action::create_generic_client(
    client_node,
    action_name,
    "test_msgs/action/Fibonacci");
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  auto send_goal_ops = rclcpp_action::GenericClient::SendGoalOptions();
  send_goal_ops.result_callback =
    [](const rclcpp_action::GenericClientGoalHandle::WrappedResult &) {};
  send_goal_ops.feedback_callback = [](
    typename rclcpp_action::GenericClientGoalHandle::SharedPtr, const void *) {};

  {
    ActionGoal goal;
    goal.order = 5;
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_take_feedback, RCL_RET_ERROR);

    auto future_goal_handle =
      action_generic_client->async_send_goal(&goal, sizeof(goal),
        send_goal_ops);
    dual_spin_until_future_complete(future_goal_handle);
    auto goal_handle = future_goal_handle.get();
    auto future_result = action_generic_client->async_get_result(goal_handle);
    EXPECT_THROW(
      dual_spin_until_future_complete(future_result),
      rclcpp::exceptions::RCLError);
  }
  {
    ActionGoal goal;
    goal.order = 5;
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_take_goal_response, RCL_RET_ERROR);

    auto future_goal_handle =
      action_generic_client->async_send_goal(&goal, sizeof(goal), send_goal_ops);
    EXPECT_THROW(
      dual_spin_until_future_complete(future_goal_handle),
      rclcpp::exceptions::RCLError);
  }
  {
    ActionGoal goal;
    goal.order = 5;
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_take_result_response, RCL_RET_ERROR);

    auto future_goal_handle =
      action_generic_client->async_send_goal(&goal, sizeof(goal), send_goal_ops);
    dual_spin_until_future_complete(future_goal_handle);
    auto goal_handle = future_goal_handle.get();
    auto future_result = action_generic_client->async_get_result(goal_handle);
    EXPECT_THROW(
      dual_spin_until_future_complete(future_result),
      rclcpp::exceptions::RCLError);
  }
  {
    ActionGoal goal;
    goal.order = 5;
    auto mock = mocking_utils::patch_and_return(
      "lib:rclcpp_action", rcl_action_take_cancel_response, RCL_RET_ERROR);

    auto future_goal_handle =
      action_generic_client->async_send_goal(&goal, sizeof(goal), send_goal_ops);
    dual_spin_until_future_complete(future_goal_handle);
    auto goal_handle = future_goal_handle.get();
    auto future_cancel_some =
      action_generic_client->async_cancel_goals_before(goal_handle->get_goal_stamp());
    EXPECT_THROW(
      dual_spin_until_future_complete(future_cancel_some),
      rclcpp::exceptions::RCLError);
  }
}

TEST_F(TestGenericClientAgainstServer,
  enable_feedback_msg_optimization_does_not_affect_normal_feedback_reception)
{
  const rcl_action_client_options_t options = rcl_action_client_get_default_options();
  auto action_generic_client =
    rclcpp_action::create_generic_client(
      client_node,
      action_name,
      "test_msgs/action/Fibonacci",
      nullptr,
      options,
      true);
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  enable_pending_handling_goal();

  constexpr size_t goal_count = 2u;
  std::array<ActionGoal, goal_count> goal_msgs;
  std::array<int, goal_count> feedback_counts{};

  using FutureGoalHandle =
    std::shared_future<typename rclcpp_action::GenericClientGoalHandle::SharedPtr>;
  std::vector<FutureGoalHandle> goal_handle_futures;
  goal_handle_futures.reserve(goal_count);

  for (size_t i = 0; i < goal_count; ++i) {
    goal_msgs[i].order = 4;
    auto send_goal_ops = rclcpp_action::GenericClient::SendGoalOptions();
    send_goal_ops.feedback_callback = [&count = feedback_counts[i]](
      typename rclcpp_action::GenericClientGoalHandle::SharedPtr, const void *)
      {
        count++;
      };
    goal_handle_futures.emplace_back(
      action_generic_client->async_send_goal(&goal_msgs[i], sizeof(goal_msgs[i]), send_goal_ops));
  }

  server_executor.spin_some(500ms);
  disable_pending_handling_goal();

  std::vector<rclcpp_action::GenericClientGoalHandle::SharedPtr> goal_handles;
  goal_handles.reserve(goal_count);

  for (auto & future_goal_handle : goal_handle_futures) {
    dual_spin_until_future_complete(future_goal_handle);
    auto goal_handle = future_goal_handle.get();
    EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());
    EXPECT_TRUE(goal_handle->is_feedback_aware());
    EXPECT_FALSE(goal_handle->is_result_aware());
    goal_handles.push_back(goal_handle);
  }

  using FutureResult = std::shared_future<rclcpp_action::GenericClient::WrappedResult>;
  std::vector<FutureResult> result_futures;
  result_futures.reserve(goal_count);

  for (auto & goal_handle : goal_handles) {
    result_futures.emplace_back(action_generic_client->async_get_result(goal_handle));
    EXPECT_TRUE(goal_handle->is_result_aware());
  }

  for (size_t i = 0; i < goal_count; ++i) {
    dual_spin_until_future_complete(result_futures[i]);
    auto wrapped_result = result_futures[i].get();
    const ActionResult * result = (const ActionResult *)(wrapped_result.result);

    EXPECT_EQ(goal_msgs[i].order + 1, feedback_counts[i]);
    EXPECT_EQ(goal_msgs[i].order + 1, result->sequence.size());
  }
}

TEST_F(TestGenericClientAgainstServer,
  enable_feedback_msg_optimization_handles_multiple_goals)
{
  if (!subscription_content_filter_supported_) {
    GTEST_SKIP() << "The feedback message optimization is only supported when rmw implementation "
      "supports subscription content filtering.";
  }

  // Skip the test if the RMW implementation is ConnextDDS.
  // ConnextDDS has restrictions on the length of content filter expressions. Please refer to
  // the definition of RMW_CONNEXT_CONTENTFILTER_PROPERTY_MAX_LENGTH. The current default value
  // is 1024, which cannot support setting 7 goal IDs. So the test will be skipped for ConnextDDS
  // to avoid failure. If the default value is increased in the future, this test can be enabled
  // for ConnextDDS as well.
  if (strcmp(rmw_get_implementation_identifier(), "rmw_connextdds") == 0) {
    GTEST_SKIP() << "RMW implementation is ConnextDDS.";
  }

  // When testing with more than 6 goals running at the same time, the internal feedback
  // subscription content filter will automatically turn off, but this does not affect the
  // reception of feedback messages for all goals.
  const rcl_action_client_options_t options = rcl_action_client_get_default_options();
  auto action_generic_client =
    rclcpp_action::create_generic_client(
      client_node,
      action_name,
      "test_msgs/action/Fibonacci",
      nullptr,
      options,
      true);
  ASSERT_TRUE(action_generic_client->wait_for_action_server(WAIT_FOR_SERVER_TIMEOUT));

  enable_pending_handling_goal();

  constexpr size_t goal_count = 7u;
  std::array<ActionGoal, goal_count> goal_msgs;
  std::array<int, goal_count> feedback_counts{};

  using FutureGoalHandle =
    std::shared_future<typename rclcpp_action::GenericClientGoalHandle::SharedPtr>;
  std::vector<FutureGoalHandle> goal_handle_futures;
  goal_handle_futures.reserve(goal_count);

  for (size_t i = 0; i < goal_count; ++i) {
    goal_msgs[i].order = static_cast<int32_t>(i + 1);
    auto send_goal_ops = rclcpp_action::GenericClient::SendGoalOptions();
    send_goal_ops.feedback_callback = [&count = feedback_counts[i]](
      typename rclcpp_action::GenericClientGoalHandle::SharedPtr, const void *)
      {
        count++;
      };
    goal_handle_futures.emplace_back(
      action_generic_client->async_send_goal(&goal_msgs[i], sizeof(goal_msgs[i]), send_goal_ops));
  }

  server_executor.spin_some(500ms);
  disable_pending_handling_goal();

  std::vector<rclcpp_action::GenericClientGoalHandle::SharedPtr> goal_handles;
  goal_handles.reserve(goal_count);

  for (auto & future_goal_handle : goal_handle_futures) {
    dual_spin_until_future_complete(future_goal_handle);
    auto goal_handle = future_goal_handle.get();
    EXPECT_EQ(rclcpp_action::GoalStatus::STATUS_ACCEPTED, goal_handle->get_status());
    EXPECT_TRUE(goal_handle->is_feedback_aware());
    EXPECT_FALSE(goal_handle->is_result_aware());
    goal_handles.push_back(goal_handle);
  }

  using FutureResult = std::shared_future<rclcpp_action::GenericClient::WrappedResult>;
  std::vector<FutureResult> result_futures;
  result_futures.reserve(goal_count);

  for (auto & goal_handle : goal_handles) {
    result_futures.emplace_back(action_generic_client->async_get_result(goal_handle));
    EXPECT_TRUE(goal_handle->is_result_aware());
  }

  for (size_t i = 0; i < goal_count; ++i) {
    dual_spin_until_future_complete(result_futures[i]);
    auto wrapped_result = result_futures[i].get();
    const ActionResult * result = (const ActionResult *)(wrapped_result.result);

    EXPECT_EQ(goal_msgs[i].order + 1, feedback_counts[i]);
    EXPECT_EQ(goal_msgs[i].order + 1, result->sequence.size());
  }
}
