#include <catch.hpp>

#include <atomic>
#include <thread>

#include "pls/pls.h"

constexpr int MAX_NUM_TASKS = 32;
constexpr int MAX_STACK_SIZE = 1024 * 8;

TEST_CASE("spawn/sync invoke calls correctly", "[algorithms/invoke.h]") {
  pls::scheduler scheduler{3, MAX_NUM_TASKS, MAX_STACK_SIZE};

  std::atomic<int> num_run{0};
  scheduler.perform_work([&] {
    pls::spawn([&] {
      num_run++;
      while (num_run < 3);
    });
    pls::spawn([&] {
      while (num_run < 1);
      num_run++;
      while (num_run < 3);
    });
    pls::spawn([&] {
      while (num_run < 2);
      num_run++;
    });
    pls::sync();
    REQUIRE(num_run == 3);
  });
}

TEST_CASE("parallel invoke calls correctly", "[algorithms/invoke.h]") {
  pls::scheduler scheduler{3, MAX_NUM_TASKS, MAX_STACK_SIZE};

  std::atomic<int> num_run{0};
  scheduler.perform_work([&] {
    pls::invoke([&] {
      num_run++;
      while (num_run < 3);
    }, [&] {
      while (num_run < 1);
      num_run++;
      while (num_run < 3);
    }, [&] {
      while (num_run < 2);
      num_run++;
    });
    REQUIRE(num_run == 3);
  });
}

TEST_CASE("parallel for calls correctly (might fail, timing based)", "[algorithms/for_each.h]") {
  pls::scheduler scheduler{8, MAX_NUM_TASKS, MAX_STACK_SIZE};

  auto start = std::chrono::steady_clock::now();
  std::atomic<int> work_done{0};
  scheduler.perform_work([&] {
    pls::for_each_range(0, 100, [&](const int) {
      work_done++;
      std::this_thread::sleep_for(std::chrono::milliseconds(1));
    });
  });
  auto end = std::chrono::steady_clock::now();
  auto elapsed =
      std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

  REQUIRE(work_done == 100);
  // It makes sense that 100 iterations on at least 4 threads take less than half the serial time.
  // We want to make sure that at least some work is distributed on multiple cores.
  REQUIRE(elapsed <= 50);
}

TEST_CASE("reduce calls correctly (might fail, timing based)", "[algorithms/for_each.h]") {
  pls::scheduler scheduler{8, MAX_NUM_TASKS, MAX_STACK_SIZE};

  auto start = std::chrono::steady_clock::now();
  int num_elements = 100;
  pls::range range{1, num_elements + 1};
  int result;
  scheduler.perform_work([&] {
    result = pls::reduce(range.begin(), range.end(), 0, [&](const int a, const int b) {
      std::this_thread::sleep_for(std::chrono::milliseconds(1));
      return a + b;
    });
  });
  auto end = std::chrono::steady_clock::now();
  auto elapsed =
      std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();

  REQUIRE(result == (num_elements * (num_elements + 1)) / 2);
  // It makes sense that 100 iterations on at least 4 threads take less than half the serial time.
  // We want to make sure that at least some work is distributed on multiple cores.
  REQUIRE(elapsed <= 50);
}
