//
// compose.cpp
// ~~~~~~~~~~~
//
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

// Disable autolinking for unit tests.
#if !defined(BOOST_ALL_NO_LIB)
#define BOOST_ALL_NO_LIB 1
#endif // !defined(BOOST_ALL_NO_LIB)

// Test that header file is self-contained.
#include <boost/asio/compose.hpp>

#include <functional>
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/system_timer.hpp>
#include "unit_test.hpp"

//------------------------------------------------------------------------------

class impl_0_completion_args
{
public:
  explicit impl_0_completion_args(boost::asio::io_context& ioc)
    : ioc_(ioc),
      state_(starting)
  {
  }

  template <typename Self>
  void operator()(Self& self)
  {
    switch (state_)
    {
    case starting:
      state_ = posting;
      boost::asio::post(ioc_, static_cast<Self&&>(self));
      break;
    case posting:
      self.complete();
      break;
    default:
      break;
    }
  }

private:
  boost::asio::io_context& ioc_;
  enum { starting, posting } state_;
};

template <typename CompletionToken>
BOOST_ASIO_INITFN_RESULT_TYPE(CompletionToken, void())
async_0_completion_args(boost::asio::io_context& ioc,
    CompletionToken&& token)
{
  return boost::asio::async_compose<CompletionToken, void()>(
      impl_0_completion_args(ioc), token);
}

void compose_0_args_handler(int* count)
{
  ++(*count);
}

struct compose_0_args_lvalue_handler
{
  int* count_;

  void operator()()
  {
    ++(*count_);
  }
};

void compose_0_completion_args_test()
{
  namespace bindns = std;

  boost::asio::io_context ioc;
  int count = 0;

  async_0_completion_args(ioc, bindns::bind(&compose_0_args_handler, &count));

  // No handlers can be called until run() is called.
  BOOST_ASIO_CHECK(!ioc.stopped());
  BOOST_ASIO_CHECK(count == 0);

  ioc.run();

  // The run() call will not return until all work has finished.
  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);

  ioc.restart();
  count = 0;

  compose_0_args_lvalue_handler lvalue_handler = { &count };
  async_0_completion_args(ioc, lvalue_handler);

  // No handlers can be called until run() is called.
  BOOST_ASIO_CHECK(!ioc.stopped());
  BOOST_ASIO_CHECK(count == 0);

  ioc.run();

  // The run() call will not return until all work has finished.
  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
}

//------------------------------------------------------------------------------

class impl_1_completion_arg
{
public:
  explicit impl_1_completion_arg(boost::asio::io_context& ioc)
    : ioc_(ioc),
      state_(starting)
  {
  }

  template <typename Self>
  void operator()(Self& self)
  {
    switch (state_)
    {
    case starting:
      state_ = posting;
      boost::asio::post(ioc_, static_cast<Self&&>(self));
      break;
    case posting:
      self.complete(42);
      break;
    default:
      break;
    }
  }

private:
  boost::asio::io_context& ioc_;
  enum { starting, posting } state_;
};

template <typename CompletionToken>
BOOST_ASIO_INITFN_RESULT_TYPE(CompletionToken, void(int))
async_1_completion_arg(boost::asio::io_context& ioc,
    CompletionToken&& token)
{
  return boost::asio::async_compose<CompletionToken, void(int)>(
      impl_1_completion_arg(ioc), token);
}

void compose_1_arg_handler(int* count, int* result_out, int result)
{
  ++(*count);
  *result_out = result;
}

struct compose_1_arg_lvalue_handler
{
  int* count_;
  int* result_out_;

  void operator()(int result)
  {
    ++(*count_);
    *result_out_ = result;
  }
};

void compose_1_completion_arg_test()
{
  namespace bindns = std;
  using bindns::placeholders::_1;

  boost::asio::io_context ioc;
  int count = 0;
  int result = 0;

  async_1_completion_arg(ioc,
      bindns::bind(&compose_1_arg_handler, &count, &result, _1));

  // No handlers can be called until run() is called.
  BOOST_ASIO_CHECK(!ioc.stopped());
  BOOST_ASIO_CHECK(count == 0);
  BOOST_ASIO_CHECK(result == 0);

  ioc.run();

  // The run() call will not return until all work has finished.
  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == 42);

  ioc.restart();
  count = 0;
  result = 0;

  compose_1_arg_lvalue_handler lvalue_handler = { &count, &result };
  async_1_completion_arg(ioc, lvalue_handler);

  // No handlers can be called until run() is called.
  BOOST_ASIO_CHECK(!ioc.stopped());
  BOOST_ASIO_CHECK(count == 0);
  BOOST_ASIO_CHECK(result == 0);

  ioc.run();

  // The run() call will not return until all work has finished.
  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == 42);
}

//------------------------------------------------------------------------------

typedef boost::asio::enable_terminal_cancellation default_filter;

template <typename CancellationFilter>
class impl_cancellable
{
public:
  explicit impl_cancellable(CancellationFilter cancellation_filter,
      boost::asio::system_timer& timer)
    : cancellation_filter_(cancellation_filter),
      timer_(timer),
      state_(starting)
  {
  }

  template <typename Self>
  void operator()(Self& self,
      const boost::system::error_code& ec = boost::system::error_code())
  {
    switch (state_)
    {
    case starting:
      if (!boost::asio::is_same<CancellationFilter, default_filter>::value)
        self.reset_cancellation_state(cancellation_filter_);
      state_ = waiting;
      timer_.expires_after(boost::asio::chrono::milliseconds(100));
      timer_.async_wait(static_cast<Self&&>(self));
      break;
    case waiting:
      self.complete(!ec);
      break;
    default:
      break;
    }
  }

private:
  CancellationFilter cancellation_filter_;
  boost::asio::system_timer& timer_;
  enum { starting, waiting } state_;
};

template <typename CancellationFilter, typename CompletionToken>
BOOST_ASIO_INITFN_RESULT_TYPE(CompletionToken, void(bool))
async_cancellable(CancellationFilter cancellation_filter,
    boost::asio::system_timer& timer,
    CompletionToken&& token)
{
  return boost::asio::async_compose<CompletionToken, void(bool)>(
      impl_cancellable<CancellationFilter>(cancellation_filter, timer), token);
}

void compose_partial_cancellation_handler(
    int* count, bool* result_out, bool result)
{
  ++(*count);
  *result_out = result;
}

void compose_default_cancellation_test()
{
  namespace bindns = std;
  using bindns::placeholders::_1;

  boost::asio::io_context ioc;
  boost::asio::system_timer timer(ioc);
  boost::asio::cancellation_signal signal;
  int count = 0;
  bool result = false;

  async_cancellable(default_filter(), timer,
      bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1));

  ioc.run();

  // No cancellation, operation completes successfully.

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == true);

  ioc.restart();
  count = 0;
  result = 0;

  async_cancellable(default_filter(), timer,
      boost::asio::bind_cancellation_slot(signal.slot(),
        bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1)));

  // Total cancellation unsupported. Operation completes successfully.
  signal.emit(boost::asio::cancellation_type::total);

  ioc.run();

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == true);

  ioc.restart();
  count = 0;
  result = 0;

  async_cancellable(default_filter(), timer,
      boost::asio::bind_cancellation_slot(signal.slot(),
        bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1)));

  // Partial cancellation unsupported. Operation completes successfully.
  signal.emit(boost::asio::cancellation_type::partial);

  ioc.run();

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == true);

  ioc.restart();
  count = 0;
  result = 0;

  async_cancellable(default_filter(), timer,
      boost::asio::bind_cancellation_slot(signal.slot(),
        bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1)));

  // Terminal cancellation works. Operation completes with failure.
  signal.emit(boost::asio::cancellation_type::terminal);

  ioc.run();

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == false);
}

void compose_partial_cancellation_test()
{
  namespace bindns = std;
  using bindns::placeholders::_1;

  boost::asio::io_context ioc;
  boost::asio::system_timer timer(ioc);
  boost::asio::cancellation_signal signal;
  int count = 0;
  bool result = false;

  async_cancellable(boost::asio::enable_partial_cancellation(), timer,
      bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1));

  ioc.run();

  // No cancellation, operation completes successfully.

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == true);

  ioc.restart();
  count = 0;
  result = 0;

  async_cancellable(boost::asio::enable_partial_cancellation(), timer,
      boost::asio::bind_cancellation_slot(signal.slot(),
        bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1)));

  // Total cancellation unsupported. Operation completes successfully.
  signal.emit(boost::asio::cancellation_type::total);

  ioc.run();

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == true);

  ioc.restart();
  count = 0;
  result = 0;

  async_cancellable(boost::asio::enable_partial_cancellation(), timer,
      boost::asio::bind_cancellation_slot(signal.slot(),
        bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1)));

  // Partial cancellation works. Operation completes with failure.
  signal.emit(boost::asio::cancellation_type::partial);

  ioc.run();

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == false);

  ioc.restart();
  count = 0;
  result = 0;

  async_cancellable(boost::asio::enable_partial_cancellation(), timer,
      boost::asio::bind_cancellation_slot(signal.slot(),
        bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1)));

  // Terminal cancellation works. Operation completes with failure.
  signal.emit(boost::asio::cancellation_type::terminal);

  ioc.run();

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == false);
}

void compose_total_cancellation_test()
{
  namespace bindns = std;
  using bindns::placeholders::_1;

  boost::asio::io_context ioc;
  boost::asio::system_timer timer(ioc);
  boost::asio::cancellation_signal signal;
  int count = 0;
  bool result = false;

  async_cancellable(boost::asio::enable_total_cancellation(), timer,
      bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1));

  ioc.run();

  // No cancellation, operation completes successfully.

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == true);

  ioc.restart();
  count = 0;
  result = 0;

  async_cancellable(boost::asio::enable_total_cancellation(), timer,
      boost::asio::bind_cancellation_slot(signal.slot(),
        bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1)));

  // Total cancellation works. Operation completes with failure.
  signal.emit(boost::asio::cancellation_type::total);

  ioc.run();

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == false);

  ioc.restart();
  count = 0;
  result = 0;

  async_cancellable(boost::asio::enable_total_cancellation(), timer,
      boost::asio::bind_cancellation_slot(signal.slot(),
        bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1)));

  // Partial cancellation works. Operation completes with failure.
  signal.emit(boost::asio::cancellation_type::partial);

  ioc.run();

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == false);

  ioc.restart();
  count = 0;
  result = 0;

  async_cancellable(boost::asio::enable_total_cancellation(), timer,
      boost::asio::bind_cancellation_slot(signal.slot(),
        bindns::bind(&compose_partial_cancellation_handler,
        &count, &result, _1)));

  // Terminal cancellation works. Operation completes with failure.
  signal.emit(boost::asio::cancellation_type::terminal);

  ioc.run();

  BOOST_ASIO_CHECK(ioc.stopped());
  BOOST_ASIO_CHECK(count == 1);
  BOOST_ASIO_CHECK(result == false);
}

//------------------------------------------------------------------------------

BOOST_ASIO_TEST_SUITE
(
  "compose",
  BOOST_ASIO_TEST_CASE(compose_0_completion_args_test)
  BOOST_ASIO_TEST_CASE(compose_1_completion_arg_test)
  BOOST_ASIO_TEST_CASE(compose_default_cancellation_test)
  BOOST_ASIO_TEST_CASE(compose_partial_cancellation_test)
  BOOST_ASIO_TEST_CASE(compose_total_cancellation_test)
)
