#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_all.hpp>

#include <algebra/TinyVector.hpp>
#include <utils/Array.hpp>
#include <utils/Messenger.hpp>

#include <utils/pugs_config.hpp>

#include <fstream>

#ifdef PUGS_HAS_MPI
#include <mpi.h>
#define IF_MPI(INSTRUCTION) INSTRUCTION
#else
#define IF_MPI(INSTRUCTION)
#endif   // PUGS_HAS_MPI

namespace mpi_check
{
struct integer
{
  int m_int;

  operator int&()
  {
    return m_int;
  }

  operator const int&() const
  {
    return m_int;
  }

  integer&
  operator=(int i)
  {
    m_int = i;
    return *this;
  }

  template <typename T>
  bool
  operator==(const T& t) const
  {
    return m_int == static_cast<int>(t);
  }
};

struct tri_int
{
  int m_int_0;
  int m_int_1;
  int m_int_2;

  bool
  operator==(tri_int t) const
  {
    return ((m_int_0 == t.m_int_0) and (m_int_1 == t.m_int_1) and (m_int_2 == t.m_int_2));
  }
};

template <typename T>
void
test_allToAll()
{
  Array<T> data_array(parallel::size());
  for (size_t i = 0; i < data_array.size(); ++i) {
    data_array[i] = parallel::rank();
  }
  auto exchanged_array = parallel::allToAll(data_array);

  for (size_t i = 0; i < data_array.size(); ++i) {
    const size_t value = exchanged_array[i];
    REQUIRE(value == i);
  }
}

template <>
void
test_allToAll<bool>()
{
  Array<bool> data_array(parallel::size());
  for (size_t i = 0; i < data_array.size(); ++i) {
    data_array[i] = ((parallel::rank() % 2) == 0);
  }
  auto exchanged_array = parallel::allToAll(data_array);

  for (size_t i = 0; i < data_array.size(); ++i) {
    REQUIRE(exchanged_array[i] == ((i % 2) == 0));
  }
}

template <>
void
test_allToAll<tri_int>()
{
  Array<tri_int> data_array(parallel::size());
  for (size_t i = 0; i < data_array.size(); ++i) {
    const int val = 1 + parallel::rank();
    data_array[i] = tri_int{val, 2 * val, val + 3};
  }
  auto exchanged_array = parallel::allToAll(data_array);

  for (size_t i = 0; i < data_array.size(); ++i) {
    const int val = 1 + i;
    REQUIRE(exchanged_array[i] == tri_int{val, 2 * val, val + 3});
  }
}

}   // namespace mpi_check

// clazy:excludeall=non-pod-global-static

TEST_CASE("Messenger", "[mpi]")
{
  SECTION("communication info")
  {
    int rank = 0;
    IF_MPI(MPI_Comm_rank(MPI_COMM_WORLD, &rank));
    REQUIRE(rank == static_cast<int>(parallel::rank()));

    int size = 1;
    IF_MPI(MPI_Comm_size(MPI_COMM_WORLD, &size));
    REQUIRE(size == static_cast<int>(parallel::size()));
  }

  SECTION("reduction")
  {
    const int min_value = parallel::allReduceMin(parallel::rank() + 3);
    REQUIRE(min_value == 3);

    const int max_value = parallel::allReduceMax(parallel::rank() + 3);
    REQUIRE(max_value == static_cast<int>((parallel::size() - 1) + 3));

    const bool and_value = parallel::allReduceAnd(true);
    REQUIRE(and_value == true);

    const bool and_value_2 = parallel::allReduceAnd(parallel::rank() > 0);
    REQUIRE(and_value_2 == false);

    const bool or_value = parallel::allReduceOr(false);
    REQUIRE(or_value == false);

    const bool or_value_2 = parallel::allReduceOr(parallel::rank() > 0);
    REQUIRE(or_value_2 == (parallel::size() > 1));

    const size_t sum_value = parallel::allReduceSum(parallel::rank() + 1);
    REQUIRE(sum_value == parallel::size() * (parallel::size() + 1) / 2);

    const TinyVector<2, size_t> sum_tiny_vector =
      parallel::allReduceSum(TinyVector<2, size_t>(parallel::rank() + 1, 1));
    REQUIRE(
      (sum_tiny_vector == TinyVector<2, size_t>{parallel::size() * (parallel::size() + 1) / 2, parallel::size()}));
  }

  SECTION("all to all")
  {
    // chars
    mpi_check::test_allToAll<char>();
    mpi_check::test_allToAll<wchar_t>();

    // integers
    mpi_check::test_allToAll<int8_t>();
    mpi_check::test_allToAll<int16_t>();
    mpi_check::test_allToAll<int32_t>();
    mpi_check::test_allToAll<int64_t>();
    mpi_check::test_allToAll<uint8_t>();
    mpi_check::test_allToAll<uint16_t>();
    mpi_check::test_allToAll<uint32_t>();
    mpi_check::test_allToAll<uint64_t>();
    mpi_check::test_allToAll<signed long long int>();
    mpi_check::test_allToAll<unsigned long long int>();

    // floats
    mpi_check::test_allToAll<float>();
    mpi_check::test_allToAll<double>();
    mpi_check::test_allToAll<long double>();

    // bools
    mpi_check::test_allToAll<bool>();

    // trivial simple type
    mpi_check::test_allToAll<mpi_check::integer>();

    // compound trivial type
    mpi_check::test_allToAll<mpi_check::tri_int>();

#ifndef NDEBUG
    SECTION("checking invalid all to all")
    {
      if (parallel::size() > 1) {
        Array<int> invalid_all_to_all(parallel::size() + 1);
        REQUIRE_THROWS_AS(parallel::allToAll(invalid_all_to_all), AssertError);

        Array<int> different_size_all_to_all(parallel::size() * (parallel::rank() + 1));
        REQUIRE_THROWS_AS(parallel::allToAll(different_size_all_to_all), AssertError);
      }
    }
#endif   // NDEBUG
  }

  SECTION("broadcast value")
  {
    {
      // simple type
      size_t value{(3 + parallel::rank()) * 2};
      parallel::broadcast(value, 0);
      REQUIRE(value == 6);
    }

    {
      // trivial simple type
      mpi_check::integer value{static_cast<int>((3 + parallel::rank()) * 2)};
      parallel::broadcast(value, 0);
      REQUIRE((value == 6));
    }

    {
      // compound trivial type
      mpi_check::tri_int value{static_cast<int>((3 + parallel::rank()) * 2), static_cast<int>(2 + parallel::rank()),
                               static_cast<int>(4 - parallel::rank())};
      parallel::broadcast(value, 0);
      REQUIRE((value == mpi_check::tri_int{6, 2, 4}));
    }
  }

  SECTION("broadcast array")
  {
    {
      // simple type
      Array<size_t> array(3);
      array[0] = (3 + parallel::rank()) * 2;
      array[1] = 2 + parallel::rank();
      array[2] = 4 - parallel::rank();
      parallel::broadcast(array, 0);
      REQUIRE(((array[0] == 6) and (array[1] == 2) and (array[2] == 4)));
    }

    {
      // trivial simple type
      Array<mpi_check::integer> array(3);
      array[0] = static_cast<int>((3 + parallel::rank()) * 2);
      array[1] = static_cast<int>(2 + parallel::rank());
      array[2] = static_cast<int>(4 - parallel::rank());
      parallel::broadcast(array, 0);
      REQUIRE(((array[0] == 6) and (array[1] == 2) and (array[2] == 4)));
    }

    {
      // compound trivial type
      Array<mpi_check::tri_int> array(3);
      array[0] = mpi_check::tri_int{static_cast<int>((3 + parallel::rank()) * 2),
                                    static_cast<int>(2 + parallel::rank()), static_cast<int>(4 - parallel::rank())};
      array[1] = mpi_check::tri_int{static_cast<int>((2 + parallel::rank()) * 4),
                                    static_cast<int>(3 + parallel::rank()), static_cast<int>(1 - parallel::rank())};
      array[2] = mpi_check::tri_int{static_cast<int>((5 + parallel::rank())), static_cast<int>(-3 + parallel::rank()),
                                    static_cast<int>(parallel::rank())};
      parallel::broadcast(array, 0);
      REQUIRE(((array[0] == mpi_check::tri_int{6, 2, 4}) and (array[1] == mpi_check::tri_int{8, 3, 1}) and
               (array[2] == mpi_check::tri_int{5, -3, 0})));
    }
  }

  SECTION("gather value to")
  {
    {
      const size_t target_rank = 10 % parallel::size();
      // simple type
      size_t value{(3 + parallel::rank()) * 2};
      Array<size_t> gather_array = parallel::gather(value, target_rank);
      if (parallel::rank() == target_rank) {
        REQUIRE(gather_array.size() == parallel::size());

        for (size_t i = 0; i < gather_array.size(); ++i) {
          REQUIRE((gather_array[i] == (3 + i) * 2));
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }

    {
      const size_t target_rank = 7 % parallel::size();
      // trivial simple type
      mpi_check::integer value{static_cast<int>((3 + parallel::rank()) * 2)};
      Array<mpi_check::integer> gather_array = parallel::gather(value, target_rank);
      if (parallel::rank() == target_rank) {
        REQUIRE(gather_array.size() == parallel::size());

        for (size_t i = 0; i < gather_array.size(); ++i) {
          const int expected_value = (3 + i) * 2;
          REQUIRE(gather_array[i] == expected_value);
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }

    {
      const size_t target_rank = 5 % parallel::size();
      // compound trivial type
      mpi_check::tri_int value{static_cast<int>((3 + parallel::rank()) * 2), static_cast<int>(2 + parallel::rank()),
                               static_cast<int>(4 - parallel::rank())};
      Array<mpi_check::tri_int> gather_array = parallel::gather(value, target_rank);

      if (parallel::rank() == target_rank) {
        REQUIRE(gather_array.size() == parallel::size());
        for (size_t i = 0; i < gather_array.size(); ++i) {
          mpi_check::tri_int expected_value{static_cast<int>((3 + i) * 2), static_cast<int>(2 + i),
                                            static_cast<int>(4 - i)};
          REQUIRE((gather_array[i] == expected_value));
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }
  }

  SECTION("gather array to")
  {
    {
      const size_t target_rank = 17 % parallel::size();
      // simple type
      Array<int> array(3);
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (3 + parallel::rank()) * 2 + i;
      }
      Array<int> gather_array = parallel::gather(array, target_rank);
      if (parallel::rank() == target_rank) {
        REQUIRE(gather_array.size() == array.size() * parallel::size());

        for (size_t i = 0; i < gather_array.size(); ++i) {
          const int expected_value = (3 + i / array.size()) * 2 + (i % array.size());
          REQUIRE((gather_array[i] == expected_value));
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }

    {
      const size_t target_rank = 13 % parallel::size();
      // trivial simple type
      Array<mpi_check::integer> array(3);
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (3 + parallel::rank()) * 2 + i;
      }
      Array<mpi_check::integer> gather_array = parallel::gather(array, target_rank);
      if (parallel::rank() == target_rank) {
        REQUIRE(gather_array.size() == array.size() * parallel::size());

        for (size_t i = 0; i < gather_array.size(); ++i) {
          const int expected_value = (3 + i / array.size()) * 2 + (i % array.size());
          REQUIRE((gather_array[i] == expected_value));
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }

    {
      const size_t target_rank = 11 % parallel::size();
      // compound trivial type
      Array<mpi_check::tri_int> array(3);
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] =
          mpi_check::tri_int{static_cast<int>((3 + parallel::rank()) * 2), static_cast<int>(2 + parallel::rank() + i),
                             static_cast<int>(4 - parallel::rank() - i)};
      }
      Array<mpi_check::tri_int> gather_array = parallel::gather(array, target_rank);

      if (parallel::rank() == target_rank) {
        REQUIRE(gather_array.size() == array.size() * parallel::size());
        for (size_t i = 0; i < gather_array.size(); ++i) {
          mpi_check::tri_int expected_value{static_cast<int>((3 + i / array.size()) * 2),
                                            static_cast<int>(2 + i / array.size() + (i % array.size())),
                                            static_cast<int>(4 - i / array.size() - (i % array.size()))};
          REQUIRE((gather_array[i] == expected_value));
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }
  }

  SECTION("gather variable array to")
  {
    {
      const size_t target_rank = 17 % parallel::size();
      // simple type
      Array<size_t> array(3 + parallel::rank());
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (parallel::rank() + i) * 2 + i;
      }
      Array<size_t> gather_array = parallel::gatherVariable(array, target_rank);

      if (parallel::rank() == target_rank) {
        const size_t gather_array_size = [&]() {
          size_t size = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            size += (3 + i_rank);
          }
          return size;
        }();

        REQUIRE(gather_array.size() == gather_array_size);

        {
          size_t i = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            for (size_t j = 0; j < i_rank + 3; ++j) {
              REQUIRE(gather_array[i++] == (i_rank + j) * 2 + j);
            }
          }
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }

    {
      const size_t target_rank = 0;
      // trivial simple type
      Array<mpi_check::integer> array(2 + 2 * parallel::rank());
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (3 + parallel::rank()) * 2 + i;
      }
      Array<mpi_check::integer> gather_array = parallel::gatherVariable(array, target_rank);

      if (parallel::rank() == target_rank) {
        const size_t gather_array_size = [&]() {
          size_t size = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            size += (2 + 2 * i_rank);
          }
          return size;
        }();

        REQUIRE(gather_array.size() == gather_array_size);

        {
          size_t i = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            for (size_t j = 0; j < 2 + 2 * i_rank; ++j) {
              REQUIRE(gather_array[i++] == (3 + i_rank) * 2 + j);
            }
          }
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }

    {
      const size_t target_rank = 13 % parallel::size();
      // compound trivial type
      Array<mpi_check::tri_int> array(3 * parallel::rank() + 1);
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] =
          mpi_check::tri_int{static_cast<int>((3 + parallel::rank()) * 2), static_cast<int>(2 + parallel::rank() + i),
                             static_cast<int>(4 - parallel::rank() - i)};
      }
      Array<mpi_check::tri_int> gather_array = parallel::gatherVariable(array, target_rank);

      if (parallel::rank() == target_rank) {
        const size_t gather_array_size = [&]() {
          size_t size = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            size += (3 * i_rank + 1);
          }
          return size;
        }();

        REQUIRE(gather_array.size() == gather_array_size);

        {
          size_t i = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            for (size_t j = 0; j < 3 * i_rank + 1; ++j) {
              auto value = mpi_check::tri_int{static_cast<int>((3 + i_rank) * 2), static_cast<int>(2 + i_rank + j),
                                              static_cast<int>(4 - i_rank - j)};

              REQUIRE(gather_array[i++] == value);
            }
          }
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }
  }

  SECTION("gather variable array to (with some empty)")
  {
    {
      const size_t target_rank = 17 % parallel::size();
      // simple type
      Array<size_t> array(3 * parallel::rank());
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (parallel::rank() + i) * 2 + i;
      }
      Array<size_t> gather_array = parallel::gatherVariable(array, target_rank);

      if (parallel::rank() == target_rank) {
        const size_t gather_array_size = [&]() {
          size_t size = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            size += (3 * i_rank);
          }
          return size;
        }();

        REQUIRE(gather_array.size() == gather_array_size);

        {
          size_t i = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            for (size_t j = 0; j < i_rank * 3; ++j) {
              REQUIRE(gather_array[i++] == (i_rank + j) * 2 + j);
            }
          }
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }

    {
      const size_t target_rank = 0;
      // trivial simple type
      Array<mpi_check::integer> array((parallel::rank() < parallel::size() - 1) ? (2 + 2 * parallel::rank()) : 0);
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (3 + parallel::rank()) * 2 + i;
      }
      Array<mpi_check::integer> gather_array = parallel::gatherVariable(array, target_rank);

      if (parallel::rank() == target_rank) {
        const size_t gather_array_size = [&]() {
          size_t size = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            size += (i_rank < parallel::size() - 1) ? (2 + 2 * i_rank) : 0;
          }
          return size;
        }();

        REQUIRE(gather_array.size() == gather_array_size);

        {
          size_t i = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            for (size_t j = 0; j < ((i_rank < parallel::size() - 1) ? (2 + 2 * i_rank) : 0); ++j) {
              REQUIRE(gather_array[i++] == (3 + i_rank) * 2 + j);
            }
          }
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }

    {
      const size_t target_rank = 13 % parallel::size();
      // compound trivial type
      Array<mpi_check::tri_int> array(3 * parallel::rank());
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] =
          mpi_check::tri_int{static_cast<int>((3 + parallel::rank()) * 2), static_cast<int>(2 + parallel::rank() + i),
                             static_cast<int>(4 - parallel::rank() - i)};
      }
      Array<mpi_check::tri_int> gather_array = parallel::gatherVariable(array, target_rank);

      if (parallel::rank() == target_rank) {
        const size_t gather_array_size = [&]() {
          size_t size = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            size += (3 * i_rank);
          }
          return size;
        }();

        REQUIRE(gather_array.size() == gather_array_size);

        {
          size_t i = 0;
          for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
            for (size_t j = 0; j < 3 * i_rank; ++j) {
              auto value = mpi_check::tri_int{static_cast<int>((3 + i_rank) * 2), static_cast<int>(2 + i_rank + j),
                                              static_cast<int>(4 - i_rank - j)};

              REQUIRE(gather_array[i++] == value);
            }
          }
        }
      } else {
        REQUIRE(gather_array.size() == 0);
      }
    }
  }

  SECTION("gather variable array to (with all empty arrays)")
  {
    {
      const size_t target_rank = 7 % parallel::size();
      // simple type [empty arrays]
      Array<size_t> array;
      Array<size_t> gather_array = parallel::gatherVariable(array, target_rank);

      REQUIRE(gather_array.size() == 0);
    }

    {
      const size_t target_rank = 0;
      // trivial simple type
      Array<mpi_check::integer> array;
      Array<mpi_check::integer> gather_array = parallel::gatherVariable(array, target_rank);

      REQUIRE(gather_array.size() == 0);
    }

    {
      const size_t target_rank = 13 % parallel::size();
      // compound trivial type
      Array<mpi_check::tri_int> array;
      Array<mpi_check::tri_int> gather_array = parallel::gatherVariable(array, target_rank);

      REQUIRE(gather_array.size() == 0);
    }
  }

  SECTION("all gather value")
  {
    {
      // simple type
      size_t value{(3 + parallel::rank()) * 2};
      Array<size_t> gather_array = parallel::allGather(value);
      REQUIRE(gather_array.size() == parallel::size());

      for (size_t i = 0; i < gather_array.size(); ++i) {
        REQUIRE((gather_array[i] == (3 + i) * 2));
      }
    }

    {
      // trivial simple type
      mpi_check::integer value{static_cast<int>((3 + parallel::rank()) * 2)};
      Array<mpi_check::integer> gather_array = parallel::allGather(value);
      REQUIRE(gather_array.size() == parallel::size());

      for (size_t i = 0; i < gather_array.size(); ++i) {
        const int expected_value = (3 + i) * 2;
        REQUIRE(gather_array[i] == expected_value);
      }
    }

    {
      // compound trivial type
      mpi_check::tri_int value{static_cast<int>((3 + parallel::rank()) * 2), static_cast<int>(2 + parallel::rank()),
                               static_cast<int>(4 - parallel::rank())};
      Array<mpi_check::tri_int> gather_array = parallel::allGather(value);

      REQUIRE(gather_array.size() == parallel::size());
      for (size_t i = 0; i < gather_array.size(); ++i) {
        mpi_check::tri_int expected_value{static_cast<int>((3 + i) * 2), static_cast<int>(2 + i),
                                          static_cast<int>(4 - i)};
        REQUIRE((gather_array[i] == expected_value));
      }
    }
  }

  SECTION("all gather array")
  {
    {
      // simple type
      Array<int> array(3);
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (3 + parallel::rank()) * 2 + i;
      }
      Array<int> gather_array = parallel::allGather(array);
      REQUIRE(gather_array.size() == array.size() * parallel::size());

      for (size_t i = 0; i < gather_array.size(); ++i) {
        const int expected_value = (3 + i / array.size()) * 2 + (i % array.size());
        REQUIRE((gather_array[i] == expected_value));
      }
    }

    {
      // trivial simple type
      Array<mpi_check::integer> array(3);
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (3 + parallel::rank()) * 2 + i;
      }
      Array<mpi_check::integer> gather_array = parallel::allGather(array);
      REQUIRE(gather_array.size() == array.size() * parallel::size());

      for (size_t i = 0; i < gather_array.size(); ++i) {
        const int expected_value = (3 + i / array.size()) * 2 + (i % array.size());
        REQUIRE((gather_array[i] == expected_value));
      }
    }

    {
      // compound trivial type
      Array<mpi_check::tri_int> array(3);
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] =
          mpi_check::tri_int{static_cast<int>((3 + parallel::rank()) * 2), static_cast<int>(2 + parallel::rank() + i),
                             static_cast<int>(4 - parallel::rank() - i)};
      }
      Array<mpi_check::tri_int> gather_array = parallel::allGather(array);

      REQUIRE(gather_array.size() == array.size() * parallel::size());
      for (size_t i = 0; i < gather_array.size(); ++i) {
        mpi_check::tri_int expected_value{static_cast<int>((3 + i / array.size()) * 2),
                                          static_cast<int>(2 + i / array.size() + (i % array.size())),
                                          static_cast<int>(4 - i / array.size() - (i % array.size()))};
        REQUIRE((gather_array[i] == expected_value));
      }
    }
  }

  SECTION("all gather empty array")
  {
    {
      // simple type
      Array<int> array;
      Array<int> gather_array = parallel::allGather(array);
      REQUIRE(gather_array.size() == 0);
    }

    {
      // trivial simple type
      Array<mpi_check::integer> array;
      Array<mpi_check::integer> gather_array = parallel::allGather(array);
      REQUIRE(gather_array.size() == 0);
    }

    {
      // compound trivial type
      Array<mpi_check::tri_int> array;
      Array<mpi_check::tri_int> gather_array = parallel::allGather(array);

      REQUIRE(gather_array.size() == 0);
    }
  }

  SECTION("all gather variable array")
  {
    {
      // simple type
      Array<size_t> array(3 + parallel::rank());
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (parallel::rank() + i) * 2 + i;
      }
      Array<size_t> gather_array = parallel::allGatherVariable(array);

      const size_t gather_array_size = [&]() {
        size_t size = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          size += (3 + i_rank);
        }
        return size;
      }();

      REQUIRE(gather_array.size() == gather_array_size);

      {
        size_t i = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          for (size_t j = 0; j < i_rank + 3; ++j) {
            REQUIRE(gather_array[i++] == (i_rank + j) * 2 + j);
          }
        }
      }
    }

    {
      // trivial simple type
      Array<mpi_check::integer> array(2 + 2 * parallel::rank());
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (3 + parallel::rank()) * 2 + i;
      }
      Array<mpi_check::integer> gather_array = parallel::allGatherVariable(array);

      const size_t gather_array_size = [&]() {
        size_t size = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          size += (2 + 2 * i_rank);
        }
        return size;
      }();

      REQUIRE(gather_array.size() == gather_array_size);

      {
        size_t i = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          for (size_t j = 0; j < 2 + 2 * i_rank; ++j) {
            REQUIRE(gather_array[i++] == (3 + i_rank) * 2 + j);
          }
        }
      }
    }

    {
      // compound trivial type
      Array<mpi_check::tri_int> array(3 * parallel::rank() + 1);
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] =
          mpi_check::tri_int{static_cast<int>((3 + parallel::rank()) * 2), static_cast<int>(2 + parallel::rank() + i),
                             static_cast<int>(4 - parallel::rank() - i)};
      }
      Array<mpi_check::tri_int> gather_array = parallel::allGatherVariable(array);

      const size_t gather_array_size = [&]() {
        size_t size = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          size += (3 * i_rank + 1);
        }
        return size;
      }();

      REQUIRE(gather_array.size() == gather_array_size);

      {
        size_t i = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          for (size_t j = 0; j < 3 * i_rank + 1; ++j) {
            auto value = mpi_check::tri_int{static_cast<int>((3 + i_rank) * 2), static_cast<int>(2 + i_rank + j),
                                            static_cast<int>(4 - i_rank - j)};

            REQUIRE(gather_array[i++] == value);
          }
        }
      }
    }
  }

  SECTION("all gather variable array to (with some empty)")
  {
    {
      // simple type
      Array<size_t> array(3 * parallel::rank());
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (parallel::rank() + i) * 2 + i;
      }
      Array<size_t> gather_array = parallel::allGatherVariable(array);

      const size_t gather_array_size = [&]() {
        size_t size = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          size += (3 * i_rank);
        }
        return size;
      }();

      REQUIRE(gather_array.size() == gather_array_size);

      {
        size_t i = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          for (size_t j = 0; j < i_rank * 3; ++j) {
            REQUIRE(gather_array[i++] == (i_rank + j) * 2 + j);
          }
        }
      }
    }

    {
      // trivial simple type
      Array<mpi_check::integer> array((parallel::rank() < parallel::size() - 1) ? (2 + 2 * parallel::rank()) : 0);
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] = (3 + parallel::rank()) * 2 + i;
      }
      Array<mpi_check::integer> gather_array = parallel::allGatherVariable(array);

      const size_t gather_array_size = [&]() {
        size_t size = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          size += (i_rank < parallel::size() - 1) ? (2 + 2 * i_rank) : 0;
        }
        return size;
      }();

      REQUIRE(gather_array.size() == gather_array_size);

      {
        size_t i = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          for (size_t j = 0; j < ((i_rank < parallel::size() - 1) ? (2 + 2 * i_rank) : 0); ++j) {
            REQUIRE(gather_array[i++] == (3 + i_rank) * 2 + j);
          }
        }
      }
    }

    {
      // compound trivial type
      Array<mpi_check::tri_int> array(3 * parallel::rank());
      for (size_t i = 0; i < array.size(); ++i) {
        array[i] =
          mpi_check::tri_int{static_cast<int>((3 + parallel::rank()) * 2), static_cast<int>(2 + parallel::rank() + i),
                             static_cast<int>(4 - parallel::rank() - i)};
      }
      Array<mpi_check::tri_int> gather_array = parallel::allGatherVariable(array);

      const size_t gather_array_size = [&]() {
        size_t size = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          size += (3 * i_rank);
        }
        return size;
      }();

      REQUIRE(gather_array.size() == gather_array_size);

      {
        size_t i = 0;
        for (size_t i_rank = 0; i_rank < parallel::size(); ++i_rank) {
          for (size_t j = 0; j < 3 * i_rank; ++j) {
            auto value = mpi_check::tri_int{static_cast<int>((3 + i_rank) * 2), static_cast<int>(2 + i_rank + j),
                                            static_cast<int>(4 - i_rank - j)};

            REQUIRE(gather_array[i++] == value);
          }
        }
      }
    }
  }

  SECTION("gather variable array to (with all empty arrays)")
  {
    {
      // simple type [empty arrays]
      Array<size_t> array;
      Array<size_t> gather_array = parallel::allGatherVariable(array);

      REQUIRE(gather_array.size() == 0);
    }

    {
      // trivial simple type
      Array<mpi_check::integer> array;
      Array<mpi_check::integer> gather_array = parallel::allGatherVariable(array);

      REQUIRE(gather_array.size() == 0);
    }

    {
      // compound trivial type
      Array<mpi_check::tri_int> array;
      Array<mpi_check::tri_int> gather_array = parallel::allGatherVariable(array);

      REQUIRE(gather_array.size() == 0);
    }
  }

  SECTION("all array exchanges")
  {
    {   // simple type
      std::vector<Array<const size_t>> send_array_list(parallel::size());
      for (size_t i = 0; i < send_array_list.size(); ++i) {
        Array<size_t> send_array(i + 1);
        for (size_t j = 0; j < send_array.size(); ++j) {
          send_array[j] = (parallel::rank() + 1) * j;
        }
        send_array_list[i] = send_array;
      }

      std::vector<Array<size_t>> recv_array_list(parallel::size());
      for (size_t i = 0; i < recv_array_list.size(); ++i) {
        recv_array_list[i] = Array<size_t>(parallel::rank() + 1);
      }
      parallel::exchange(send_array_list, recv_array_list);

      for (size_t i = 0; i < parallel::size(); ++i) {
        const Array<const size_t> recv_array = recv_array_list[i];
        for (size_t j = 0; j < recv_array.size(); ++j) {
          REQUIRE(recv_array[j] == (i + 1) * j);
        }
      }
    }

    {   //  trivial simple type
      std::vector<Array<mpi_check::integer>> send_array_list(parallel::size());
      for (size_t i = 0; i < send_array_list.size(); ++i) {
        Array<mpi_check::integer> send_array(i + 1);
        for (size_t j = 0; j < send_array.size(); ++j) {
          send_array[j] = static_cast<int>((parallel::rank() + 1) * j);
        }
        send_array_list[i] = send_array;
      }

      std::vector<Array<mpi_check::integer>> recv_array_list(parallel::size());
      for (size_t i = 0; i < recv_array_list.size(); ++i) {
        recv_array_list[i] = Array<mpi_check::integer>(parallel::rank() + 1);
      }
      parallel::exchange(send_array_list, recv_array_list);

      for (size_t i = 0; i < parallel::size(); ++i) {
        const Array<const mpi_check::integer> recv_array = recv_array_list[i];
        for (size_t j = 0; j < recv_array.size(); ++j) {
          REQUIRE(recv_array[j] == (i + 1) * j);
        }
      }
    }

    {
      // compound trivial type
      std::vector<Array<mpi_check::tri_int>> send_array_list(parallel::size());
      for (size_t i = 0; i < send_array_list.size(); ++i) {
        Array<mpi_check::tri_int> send_array(i + 1);
        for (size_t j = 0; j < send_array.size(); ++j) {
          send_array[j] = mpi_check::tri_int{static_cast<int>((parallel::rank() + 1) * j),
                                             static_cast<int>(parallel::rank()), static_cast<int>(j)};
        }
        send_array_list[i] = send_array;
      }

      std::vector<Array<mpi_check::tri_int>> recv_array_list(parallel::size());
      for (size_t i = 0; i < recv_array_list.size(); ++i) {
        recv_array_list[i] = Array<mpi_check::tri_int>(parallel::rank() + 1);
      }
      parallel::exchange(send_array_list, recv_array_list);

      for (size_t i = 0; i < parallel::size(); ++i) {
        const Array<const mpi_check::tri_int> recv_array = recv_array_list[i];
        for (size_t j = 0; j < recv_array.size(); ++j) {
          mpi_check::tri_int expected_value{static_cast<int>((i + 1) * j), static_cast<int>(i), static_cast<int>(j)};
          REQUIRE((recv_array[j] == expected_value));
        }
      }
    }
  }

#ifndef NDEBUG
  SECTION("checking all array exchange invalid sizes")
  {
    std::vector<Array<const int>> send_array_list(parallel::size());
    for (size_t i = 0; i < send_array_list.size(); ++i) {
      Array<int> send_array(i + 1);
      send_array.fill(parallel::rank());
      send_array_list[i] = send_array;
    }

    std::vector<Array<int>> recv_array_list(parallel::size());
    REQUIRE_THROWS_AS(parallel::exchange(send_array_list, recv_array_list), AssertError);
  }
#endif   // NDEBUG

  SECTION("checking barrier")
  {
    for (size_t i = 0; i < parallel::size(); ++i) {
      if (i == parallel::rank()) {
        std::ofstream file;
        if (i == 0) {
          file.open("barrier_test", std::ios_base::out);
        } else {
          file.open("barrier_test", std::ios_base::app);
        }
        file << i << "\n" << std::flush;
      }
      parallel::barrier();
    }

    {   // reading produced file
      std::ifstream file("barrier_test");
      std::vector<size_t> number_list;
      while (file) {
        size_t value;
        file >> value;
        if (file) {
          number_list.push_back(value);
        }
      }
      REQUIRE(number_list.size() == parallel::size());
      for (size_t i = 0; i < number_list.size(); ++i) {
        REQUIRE(number_list[i] == i);
      }
    }
    parallel::barrier();

    std::remove("barrier_test");
  }

  SECTION("errors")
  {
    int argc    = 0;
    char** argv = nullptr;
    REQUIRE_THROWS_WITH((parallel::Messenger::create(argc, argv)), "unexpected error: Messenger already created");
  }
}