#include "doctest/doctest.h"
#include "misc/DocTestExtensions.h"

#include "quill/core/BoundedSPSCQueue.h"
#include "quill/core/QuillError.h"

#include <cstring>
#include <limits>
#include <thread>
#include <vector>

#if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
  #pragma warning(push)
  #pragma warning(disable : 4996)
#endif

TEST_SUITE_BEGIN("BoundedQueue");

using namespace quill::detail;

#if !defined(QUILL_X86ARCH)
// QUILL_X86ARCH requires at least a queue capacity of 1024 and those tests are using a smaller number

TEST_CASE("read_write_buffer")
{
  BoundedSPSCQueue buffer{64u};

  for (uint32_t i = 0; i < 128; ++i)
  {
    {
      std::byte* write_buf = buffer.prepare_write(32u);
      REQUIRE_NE(write_buf, nullptr);
      buffer.finish_write(32u);
      buffer.commit_write();
    }

    {
      std::byte* write_buf = buffer.prepare_write(32u);
      REQUIRE_NE(write_buf, nullptr);
      buffer.finish_write(32u);
      buffer.commit_write();
    }

    {
      std::byte* res = buffer.prepare_read();
      REQUIRE(res);
      buffer.finish_read(32u);
      buffer.commit_read();
    }

    {
      std::byte* res = buffer.prepare_read();
      REQUIRE(res);
      buffer.finish_read(32u);
      buffer.commit_read();

      res = buffer.prepare_read();
      REQUIRE_FALSE(res);
    }
  }

  std::byte* res = buffer.prepare_read();
  REQUIRE_FALSE(res);
}

TEST_CASE("bounded_queue_integer_overflow")
{
  BoundedSPSCQueueImpl<uint8_t> buffer{128, quill::HugePagesPolicy::Never, 0};
  size_t constexpr iterations = static_cast<size_t>(std::numeric_limits<uint8_t>::max()) * 8ull;

  for (size_t i = 0; i < iterations; ++i)
  {
    std::string to_write{"test"};
    to_write += std::to_string(i);
    std::byte* r = buffer.prepare_write(static_cast<uint8_t>(to_write.length()) + static_cast<uint8_t>(1));
    std::strncpy(reinterpret_cast<char*>(r), to_write.data(), to_write.length() + 1);
    buffer.finish_write(static_cast<uint8_t>(to_write.length()) + static_cast<uint8_t>(1));
    buffer.commit_write();

    // now read
    std::byte* w = buffer.prepare_read();
    REQUIRE(w);
    char result[256];
    std::memcpy(&result[0], w, static_cast<uint8_t>(to_write.length()) + static_cast<uint8_t>(1));
    REQUIRE_STREQ(result, to_write.data());
    buffer.finish_read(static_cast<uint8_t>(to_write.length()) + static_cast<uint8_t>(1));
    buffer.commit_read();
  }
}
#endif

TEST_CASE("bounded_queue_read_write_multithreaded_plain_ints")
{
  BoundedSPSCQueue buffer{131'072};

  std::thread producer_thread(
    [&buffer]()
    {
      for (uint32_t wrap_cnt = 0; wrap_cnt < 20; ++wrap_cnt)
      {
        for (uint32_t i = 0; i < 8192; ++i)
        {
          std::byte* write_buffer = buffer.prepare_write(sizeof(uint32_t));

          while (!write_buffer)
          {
            std::this_thread::sleep_for(std::chrono::microseconds{2});
            write_buffer = buffer.prepare_write(sizeof(uint32_t));
          }

          std::memcpy(write_buffer, &i, sizeof(uint32_t));
          buffer.finish_write(sizeof(uint32_t));
          buffer.commit_write();
        }
      }
    });

  std::thread consumer_thread(
    [&buffer]()
    {
      for (uint32_t wrap_cnt = 0; wrap_cnt < 20; ++wrap_cnt)
      {
        for (uint32_t i = 0; i < 8192; ++i)
        {
          std::byte const* read_buffer = buffer.prepare_read();
          while (!read_buffer)
          {
            std::this_thread::sleep_for(std::chrono::microseconds{2});
            read_buffer = buffer.prepare_read();
          }

          auto const value = reinterpret_cast<uint32_t const*>(read_buffer);
          REQUIRE_EQ(*value, i);
          buffer.finish_read(sizeof(uint32_t));
          buffer.commit_read();
        }
      }
    });

  producer_thread.join();
  consumer_thread.join();
}

TEST_CASE("bounded_queue_prepare_write_larger_than_capacity_fails")
{
  BoundedSPSCQueue buffer{64u};

  REQUIRE_EQ(buffer.capacity(), 64u);
  REQUIRE_EQ(buffer.prepare_write(65u), nullptr);
  REQUIRE_EQ(buffer.prepare_write(128u), nullptr);

  std::byte* write_buf = buffer.prepare_write(32u);
  REQUIRE_NE(write_buf, nullptr);
  buffer.finish_write(32u);
  buffer.commit_write();

  std::byte* read_buf = buffer.prepare_read();
  REQUIRE_NE(read_buf, nullptr);
  buffer.finish_read(32u);
  buffer.commit_read();

  REQUIRE_EQ(buffer.prepare_write(65u), nullptr);
  REQUIRE_EQ(buffer.prepare_write(128u), nullptr);
}

TEST_CASE("bounded_queue_write_after_drain_uses_full_capacity")
{
  // default reader_store_percent 5 -> the reader publishes its position every 204 bytes
  BoundedSPSCQueue buffer{4096u};

  // consume a message smaller than the publish batch, draining the queue
  std::byte* write_buf = buffer.prepare_write(100u);
  REQUIRE_NE(write_buf, nullptr);
  buffer.finish_write(100u);
  buffer.commit_write();

  std::byte* read_buf = buffer.prepare_read();
  REQUIRE_NE(read_buf, nullptr);
  buffer.finish_read(100u);
  buffer.commit_read();

  // the queue is empty; a message that fits in the capacity must be writable. If the reader
  // position was not published on drain, the producer permanently sees only (capacity - 100)
  // free bytes and this write can never succeed
  REQUIRE_EQ(buffer.prepare_read(), nullptr);
  REQUIRE_NE(buffer.prepare_write(4000u), nullptr);
}

#if defined(QUILL_X86ARCH) && !defined(QUILL_NO_EXCEPTIONS)
TEST_CASE("below_minimum_capacity_throws_before_allocating")
{
  // On x86 the constructor requires at least 1024 bytes so that cache-line warmup via
  // _mm_prefetch has enough backing memory. The capacity check must run in the member
  // initialiser list BEFORE mmap/aligned_malloc happens, otherwise the allocation would
  // leak (the destructor does not run on a partially-constructed object).
  REQUIRE_THROWS_AS(BoundedSPSCQueue{512u}, quill::QuillError);
  REQUIRE_THROWS_AS(BoundedSPSCQueue{1023u}, quill::QuillError);

  // Exactly 1024 is the documented minimum and must succeed.
  REQUIRE_NOTHROW(BoundedSPSCQueue{1024u});
}
#endif

#if !defined(QUILL_NO_EXCEPTIONS)
TEST_CASE("oversized_capacity_throws_before_allocating")
{
  size_t constexpr oversized_capacity = (std::numeric_limits<size_t>::max() / 2u) + 1u;
  REQUIRE_THROWS_AS(BoundedSPSCQueue{oversized_capacity}, quill::QuillError);
}
#endif

TEST_SUITE_END();

#if defined(_WIN32) && defined(_MSC_VER) && !defined(__GNUC__)
  #pragma warning(pop)
#endif
