#include "doctest/doctest.h"

#include "misc/TestUtilities.h"
#include "quill/Backend.h"
#include "quill/DeferredFormatCodec.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/sinks/FileSink.h"

#include "quill/std/Optional.h"

#include <array>
#include <cstdio>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

using namespace quill;

class OptionalCopyOnlyType
{
public:
  explicit OptionalCopyOnlyType(std::string label, int value)
    : label(std::move(label)), value(value)
  {
  }

  OptionalCopyOnlyType(OptionalCopyOnlyType const&) = default;
  OptionalCopyOnlyType& operator=(OptionalCopyOnlyType const&) = default;
  OptionalCopyOnlyType(OptionalCopyOnlyType&&) = delete;
  OptionalCopyOnlyType& operator=(OptionalCopyOnlyType&&) = delete;

  std::string label;
  int value;
};

template <>
struct fmtquill::formatter<OptionalCopyOnlyType>
{
  template <typename FormatContext>
  constexpr auto parse(FormatContext& ctx)
  {
    return ctx.begin();
  }

  template <typename FormatContext>
  auto format(::OptionalCopyOnlyType const& optional_copy_only_type, FormatContext& ctx) const
  {
    return fmtquill::format_to(ctx.out(), "OptionalCopyOnly(label: {}, value: {})",
                               optional_copy_only_type.label, optional_copy_only_type.value);
  }
};

template <>
struct quill::Codec<OptionalCopyOnlyType> : quill::DeferredFormatCodec<OptionalCopyOnlyType>
{
};

/***/
TEST_CASE("std_optional_logging")
{
  static constexpr char const* filename = "std_optional_logging.log";
  static std::string const logger_name = "logger";

  // Start the logging backend thread
  Backend::start();

  Frontend::preallocate();

  // Set writing logging to a file
  auto file_sink = Frontend::create_or_get_sink<FileSink>(
    filename,
    []()
    {
      FileSinkConfig cfg;
      cfg.set_open_mode('w');
      return cfg;
    }(),
    FileEventNotifier{});

  Logger* logger = Frontend::create_or_get_logger(logger_name, std::move(file_sink));

  {
    std::optional<int> ei{std::nullopt};
    LOG_INFO(logger, "ei [{}]", ei);

    std::optional<double> ed{std::nullopt};
    LOG_INFO(logger, "ed [{}]", ed);

    std::optional<char const*> ccp{"testing"};
    LOG_INFO(logger, "ccp [{}]", ccp);

    std::optional<std::string> sp{"sp_testing"};
    LOG_INFO(logger, "sp [{}]", sp);

    std::optional<std::string> esp{std::nullopt};
    LOG_INFO(logger, "esp [{}]", esp);
#if !defined(__clang__) && defined(__GNUC__)
  #pragma GCC diagnostic push
  #pragma GCC diagnostic ignored "-Wunused-value"
#endif
    std::optional<std::string_view> svp{"svp_testing"};
    LOG_INFO(logger, "svp [{}]", svp);

    std::optional<int> i{123321};
    LOG_INFO(logger, "i [{}]", i);

    std::optional<double> d{333.221};
    LOG_INFO(logger, "d [{}]", d);
    LOG_INFO(logger, "zzzz [{}] [{}] [{}] [{}] [{}]", d, "test", *svp, *sp, ccp);
#if !defined(__clang__) && defined(__GNUC__)
  #pragma GCC diagnostic pop
#endif

    // Test rvalue references with optional
    std::optional<int> rvalue_opt = 100;
    LOG_INFO(logger, "rvalue_opt {}", std::move(rvalue_opt));

    // Test with temporary optional
    LOG_INFO(logger, "temp_opt {}", std::optional<std::string>{"temporary"});

    std::optional<OptionalCopyOnlyType> copy_only_opt{std::in_place, "copy_only", 42};
    LOG_INFO(logger, "copy_only_opt {}", copy_only_opt);

    LOG_INFO(logger, "temp_copy_only_opt {}",
             std::optional<OptionalCopyOnlyType>{std::in_place, "temp_copy_only", 84});
  }

  logger->flush_log();
  Frontend::remove_logger(logger);

  // Wait until the backend thread stops for test stability
  Backend::stop();

  // Read file and check
  std::vector<std::string> const file_contents = quill::testing::file_contents(filename);

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       ei [none]"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       ed [none]"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       ccp [optional(\"testing\")]"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       sp [optional(\"sp_testing\")]"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       esp [none]"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       svp [optional(\"svp_testing\")]"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       i [optional(123321)]"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       d [optional(333.221)]"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       zzzz [optional(333.221)] [test] [svp_testing] [sp_testing] [optional(\"testing\")]"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       rvalue_opt optional(100)"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       temp_opt optional(\"temporary\")"}));

  REQUIRE(quill::testing::file_contains(
    file_contents,
    std::string{"LOG_INFO      " + logger_name +
                "       copy_only_opt optional(OptionalCopyOnly(label: copy_only, value: 42))"}));

  REQUIRE(quill::testing::file_contains(
    file_contents, std::string{"LOG_INFO      " + logger_name + "       temp_copy_only_opt optional(OptionalCopyOnly(label: temp_copy_only, value: 84))"}));

  testing::remove_file(filename);
}
