/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "gtest/gtest.h"
#include "fmt/format.h"
#include "fmt/xchar.h"
#include "nsTArray.h"
#include "nsFmtString.h"
#include "nsString.h"
#include "mozilla/Sprintf.h"

#define snprintf(...)

namespace tfformat {
#include "../glibc_printf_tests/tfformat.c"
}

#undef snprintf

nsCString PrintfToFmtFormat(const char* aPrintfFormatString) {
  nsCString fmtFormat;
  fmtFormat.AppendASCII("{");
  fmtFormat.AppendASCII(aPrintfFormatString);
  // {fmt} uses < to left align, while printf traditionally use -.
  // The order between the sign forcing ("+") or zero-padding ("0") and the
  // alignment ("-" or "<") is also reversed.
  fmtFormat.ReplaceSubstring("+-", "<+");
  fmtFormat.ReplaceSubstring("0-", "<0");
  // {fmt} doesn't support e.g. %4.f to denote 4 digits of integer part, and
  // zero digit of fractional part. Simply replace them by the explicit form
  // with a 0.
  fmtFormat.ReplaceSubstring(".f", ".0f");
  fmtFormat.ReplaceSubstring(".F", ".0F");
  fmtFormat.ReplaceSubstring(".e", ".0e");
  fmtFormat.ReplaceSubstring(".E", ".0E");
  fmtFormat.ReplaceSubstring(".G", ".0G");
  fmtFormat.ReplaceSubstring(".g", ".0g");
  fmtFormat.ReplaceChar('%', ':');
  fmtFormat.AppendASCII("}");
  return fmtFormat;
}

TEST(Fmt, CrossCheckPrintf)
{
  char bufGeckoPrintf[1024] = {};
  // - 1 because the last item is just a zero
  for (uint32_t i = 2; i < std::size(tfformat::sprint_doubles) - 1; i++) {
    if (strstr(tfformat::sprint_doubles[i].format_string, "#") ||
        strstr(tfformat::sprint_doubles[i].format_string, "a")) {
      // Gecko's printf doesn't implement the '#' specifier nor 'a' conversion
      // specifier (hex notation), but {fmt} does.
      // Skip this test-case for the cross-check.
      continue;
    }
    nsCString fmtFormat =
        PrintfToFmtFormat(tfformat::sprint_doubles[i].format_string);
    nsCString withFmt;
    withFmt.AppendVfmt(fmtFormat.get(), fmt::make_format_args(
                                            tfformat::sprint_doubles[i].value));
    SprintfBuf(bufGeckoPrintf, 1024, tfformat::sprint_doubles[i].format_string,
               tfformat::sprint_doubles[i].value);
    if (strcmp(bufGeckoPrintf, withFmt.get()) != 0) {
      fprintf(stderr, "Conversion index %d: %lf, %s -> %s\n", i,
              tfformat::sprint_doubles[i].value,
              tfformat::sprint_doubles[i].format_string, fmtFormat.get());
      fprintf(stderr, "DIFF: |%s|\n vs.   |%s|\n", bufGeckoPrintf,
              withFmt.get());
    }
    ASSERT_STREQ(bufGeckoPrintf, withFmt.get());
  }
}

TEST(Fmt, Sequences)
{
  char bufFmt[1024] = {};
  {
    nsTArray<int> array(4);
    for (uint32_t i = 0; i < 4; i++) {
      array.AppendElement(i);
    }
    auto [out, size] = fmt::format_to(bufFmt, "{}", fmt::join(array, ", "));
    *out = 0;
    ASSERT_STREQ("0, 1, 2, 3", bufFmt);
  }
  {
    nsTArray<uint8_t> array(4);
    for (uint32_t i = 0; i < 4; i++) {
      array.AppendElement((123 * 5 * (i + 1)) % 255);
    }
    auto [out, size] =
        fmt::format_to(bufFmt, "{:#04x}", fmt::join(array, ", "));
    *out = 0;
    ASSERT_STREQ("0x69, 0xd2, 0x3c, 0xa5", bufFmt);
  }
}

struct POD {
  double mA;
  uint64_t mB;
};

struct POD2 {
  double mA;
  uint64_t mB;
};

template <>
class fmt::formatter<POD> : public formatter<string_view> {
 public:
  auto format(const POD& aPod, format_context& aCtx) const {
    std::string temp;
    fmt::format_to(std::back_inserter(temp), "POD: mA: {}, mB: {}", aPod.mA,
                   aPod.mB);
    return formatter<string_view>::format(temp, aCtx);
  }
};

auto format_as(POD2 aInstance) -> std::string {
  return fmt::format("POD2: mA: {}, mB: {}", aInstance.mA, aInstance.mB);
}

TEST(Fmt, PodPrint)
{
  char bufFmt[1024] = {};

  POD p{4.33, 8};
  POD2 p2{4.33, 8};
  {
    auto [out, size] = fmt::format_to(bufFmt, "{}", p);
    *out = 0;
    ASSERT_STREQ("POD: mA: 4.33, mB: 8", bufFmt);
  }

  {
    auto [out, size] = fmt::format_to(bufFmt, "{:>30}", p);
    *out = 0;
    ASSERT_STREQ("          POD: mA: 4.33, mB: 8", bufFmt);
  }

  {
    auto [out, size] = fmt::format_to(bufFmt, "{:>30}", p2);
    *out = 0;
    ASSERT_STREQ("         POD2: mA: 4.33, mB: 8", bufFmt);
  }
}

TEST(Fmt, nsString)
{
  {
    nsFmtCString str("{} {} {}", 4, 4.3, " end");
    ASSERT_STREQ("4 4.3  end", str.get());
  }
  {
    nsFmtString str(u"Étonnant {} {} {}", u"Étienne", 4, 4.3);
    ASSERT_STREQ("Étonnant Étienne 4 4.3", NS_ConvertUTF16toUTF8(str).get());
  }
  {
    nsString str;
    str.AppendFmt(u"Étonnant {} {} {}", u"Étienne", 4, 4.3);
    ASSERT_STREQ("Étonnant Étienne 4 4.3", NS_ConvertUTF16toUTF8(str).get());
  }
  {
    nsCString str;
    str.AppendFmt("{} {} {}", 4, 4.3, " end");
    ASSERT_STREQ("4 4.3  end", str.get());
  }
}

TEST(Fmt, Truncation)
{
  char too_short_buf[16];
  const char* too_long_buf = "asdasdlkasjdashdkajhsdkhaksdjhasd";
  {
    auto [out, truncated] = fmt::format_to(too_short_buf, "{}", too_long_buf);
    assert(truncated);
    // Overwrite the last char for printing
    too_short_buf[15] = 0;
    fmt::print("{} {} {}\n", too_short_buf, fmt::ptr(out), truncated);
  }
  {
    auto [out, size] = fmt::format_to_n(too_short_buf, sizeof(too_short_buf),
                                        "{}", too_long_buf);
    assert(size > sizeof(too_short_buf));
    too_short_buf[15] = 0;
    fmt::print("{} {} {}\n", too_short_buf, fmt::ptr(out), size);
  }
}

TEST(Fmt, NullString)
{
  char str[16];
  auto [out, size] = fmt::format_to(str, "{}", (char*)nullptr);
  *out = 0;
  ASSERT_STREQ("(null)", str);
}

// ASAN intercepts the underlying fwrite and crashes.
#if defined(__has_feature)
#  if not(__has_feature(address_sanitizer))
TEST(Fmt, IOError)
{
  FILE* duped = fdopen(dup(fileno(stderr)), "w");
  fclose(duped);
  // glibc will crash here on x86 Linux debug
#    if defined(DEBUG) && defined(XP_LINUX) && !defined(__i386__)
  fmt::println(duped, "Hi {}", 14);
#    endif
}
#  endif
#endif
