/* 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 "mozilla/intl/Calendar.h"
#include "mozilla/intl/DateTimeFormat.h"
#include "mozilla/intl/DateTimePart.h"
#include "mozilla/intl/DateTimePatternGenerator.h"
#include "mozilla/intl/Locale.h"
#include "mozilla/Span.h"
#include "TestBuffer.h"

#include <string_view>

namespace mozilla::intl {

// Firefox 1.0 release date.
const double DATE = 1032800850000.0;

static UniquePtr<DateTimeFormat> testStyle(
    const char* aLocale, DateTimeFormat::StyleBag& aStyleBag) {
  // Always specify a time zone in the tests, otherwise it will use the system
  // time zone which can vary between test runs.
  auto timeZone = Some(MakeStringSpan(u"GMT+3"));
  auto gen = DateTimePatternGenerator::TryCreate("en").unwrap();
  return DateTimeFormat::TryCreateFromStyle(MakeStringSpan(aLocale), aStyleBag,
                                            gen.get(), timeZone)
      .unwrap();
}

TEST(IntlDateTimeFormat, Style_enUS_utf8)
{
  DateTimeFormat::StyleBag style;
  style.date = Some(DateTimeFormat::Style::Medium);
  style.time = Some(DateTimeFormat::Style::Medium);

  auto dtFormat = testStyle("en-US", style);
  TestBuffer<char> buffer;
  dtFormat->TryFormat(DATE, buffer).unwrap();

  ASSERT_TRUE(buffer.verboseMatches("Sep 23, 2002, 8:07:30 PM"));
}

TEST(IntlDateTimeFormat, Style_enUS_utf16)
{
  DateTimeFormat::StyleBag style;
  style.date = Some(DateTimeFormat::Style::Medium);
  style.time = Some(DateTimeFormat::Style::Medium);

  auto dtFormat = testStyle("en-US", style);
  TestBuffer<char16_t> buffer;
  dtFormat->TryFormat(DATE, buffer).unwrap();

  ASSERT_TRUE(buffer.verboseMatches(u"Sep 23, 2002, 8:07:30 PM"));
}

TEST(IntlDateTimeFormat, Style_ar_utf8)
{
  DateTimeFormat::StyleBag style;
  style.time = Some(DateTimeFormat::Style::Medium);

  auto dtFormat = testStyle("ar-EG", style);
  TestBuffer<char> buffer;
  dtFormat->TryFormat(DATE, buffer).unwrap();

  ASSERT_TRUE(buffer.verboseMatches("٨:٠٧:٣٠ م"));
}

TEST(IntlDateTimeFormat, Style_ar_utf16)
{
  DateTimeFormat::StyleBag style;
  style.time = Some(DateTimeFormat::Style::Medium);

  auto dtFormat = testStyle("ar-EG", style);
  TestBuffer<char16_t> buffer;
  dtFormat->TryFormat(DATE, buffer).unwrap();

  ASSERT_TRUE(buffer.verboseMatches(u"٨:٠٧:٣٠ م"));
}

TEST(IntlDateTimeFormat, Style_enUS_fallback_to_default_styles)
{
  DateTimeFormat::StyleBag style;

  auto dtFormat = testStyle("en-US", style);
  TestBuffer<char> buffer;
  dtFormat->TryFormat(DATE, buffer).unwrap();

  ASSERT_TRUE(buffer.verboseMatches("Sep 23, 2002, 8:07:30 PM"));
}

TEST(IntlDateTimeFormat, Time_zone_IANA_identifier)
{
  auto gen = DateTimePatternGenerator::TryCreate("en").unwrap();

  DateTimeFormat::StyleBag style;
  style.date = Some(DateTimeFormat::Style::Medium);
  style.time = Some(DateTimeFormat::Style::Medium);

  auto dtFormat = DateTimeFormat::TryCreateFromStyle(
                      MakeStringSpan("en-US"), style, gen.get(),
                      Some(MakeStringSpan(u"America/Chicago")))
                      .unwrap();
  TestBuffer<char> buffer;
  dtFormat->TryFormat(DATE, buffer).unwrap();
  ASSERT_TRUE(buffer.verboseMatches("Sep 23, 2002, 12:07:30 PM"));
}

TEST(IntlDateTimeFormat, GetAllowedHourCycles)
{
  auto language = [](const char* s) {
    LanguageSubtag result;
    result.Set(MakeStringSpan(s));
    return result;
  };

  auto region = [](const char* s) {
    RegionSubtag result;
    result.Set(MakeStringSpan(s));
    return result;
  };

  auto allowed_en_US =
      DateTimeFormat::GetAllowedHourCycles(language("en"), region("US"))
          .unwrap();

  ASSERT_TRUE(allowed_en_US.length() == 2);
  ASSERT_EQ(allowed_en_US[0], DateTimeFormat::HourCycle::H12);
  ASSERT_EQ(allowed_en_US[1], DateTimeFormat::HourCycle::H23);

  auto allowed_de_DE =
      DateTimeFormat::GetAllowedHourCycles(language("de"), region("DE"))
          .unwrap();

  ASSERT_TRUE(allowed_de_DE.length() == 2);
  ASSERT_EQ(allowed_de_DE[0], DateTimeFormat::HourCycle::H23);
  ASSERT_EQ(allowed_de_DE[1], DateTimeFormat::HourCycle::H12);

  auto allowed_en_CA =
      DateTimeFormat::GetAllowedHourCycles(language("en"), region("CA"))
          .unwrap();

  ASSERT_TRUE(allowed_en_CA.length() == 2);
  ASSERT_EQ(allowed_en_CA[0], DateTimeFormat::HourCycle::H12);
  ASSERT_EQ(allowed_en_CA[1], DateTimeFormat::HourCycle::H23);

  auto allowed_fr_CA =
      DateTimeFormat::GetAllowedHourCycles(language("fr"), region("CA"))
          .unwrap();

  ASSERT_TRUE(allowed_fr_CA.length() == 2);
  ASSERT_EQ(allowed_fr_CA[0], DateTimeFormat::HourCycle::H23);
  ASSERT_EQ(allowed_fr_CA[1], DateTimeFormat::HourCycle::H12);
}

TEST(IntlDateTimePatternGenerator, GetBestPattern)
{
  auto gen = DateTimePatternGenerator::TryCreate("en").unwrap();
  TestBuffer<char16_t> buffer;

  gen->GetBestPattern(MakeStringSpan(u"yMd"), buffer).unwrap();
  ASSERT_TRUE(buffer.verboseMatches(u"M/d/y"));
}

TEST(IntlDateTimePatternGenerator, GetSkeleton)
{
  auto gen = DateTimePatternGenerator::TryCreate("en").unwrap();
  TestBuffer<char16_t> buffer;

  DateTimePatternGenerator::GetSkeleton(MakeStringSpan(u"M/d/y"), buffer)
      .unwrap();
  ASSERT_TRUE(buffer.verboseMatches(u"yMd"));
}

TEST(IntlDateTimePatternGenerator, GetPlaceholderPattern)
{
  auto gen = DateTimePatternGenerator::TryCreate("en").unwrap();
  auto span = gen->GetPlaceholderPattern();
  // The default date-time pattern for 'en' locale is u"{1}, {0}".
  ASSERT_EQ(span, MakeStringSpan(u"{1}, {0}"));
}

// A utility function to help test the DateTimeFormat::ComponentsBag.
[[nodiscard]] bool FormatComponents(
    TestBuffer<char16_t>& aBuffer, DateTimeFormat::ComponentsBag& aComponents,
    Span<const char> aLocale = MakeStringSpan("en-US")) {
  UniquePtr<DateTimePatternGenerator> gen = nullptr;
  auto dateTimePatternGenerator =
      DateTimePatternGenerator::TryCreate(aLocale.data()).unwrap();

  auto dtFormat = DateTimeFormat::TryCreateFromComponents(
      aLocale, aComponents, dateTimePatternGenerator.get(),
      Some(MakeStringSpan(u"GMT+3")));
  if (dtFormat.isErr()) {
    fprintf(stderr, "Could not create a DateTimeFormat\n");
    return false;
  }

  auto result = dtFormat.unwrap()->TryFormat(DATE, aBuffer);
  if (result.isErr()) {
    fprintf(stderr, "Could not format a DateTimeFormat\n");
    return false;
  }

  return true;
}

TEST(IntlDateTimeFormat, Components)
{
  DateTimeFormat::ComponentsBag components{};

  components.year = Some(DateTimeFormat::Numeric::Numeric);
  components.month = Some(DateTimeFormat::Month::Numeric);
  components.day = Some(DateTimeFormat::Numeric::Numeric);

  components.hour = Some(DateTimeFormat::Numeric::Numeric);
  components.minute = Some(DateTimeFormat::Numeric::TwoDigit);
  components.second = Some(DateTimeFormat::Numeric::TwoDigit);

  TestBuffer<char16_t> buffer;
  ASSERT_TRUE(FormatComponents(buffer, components));
  ASSERT_TRUE(buffer.verboseMatches(u"9/23/2002, 8:07:30 PM"));
}

TEST(IntlDateTimeFormat, Components_es_ES)
{
  DateTimeFormat::ComponentsBag components{};

  components.year = Some(DateTimeFormat::Numeric::Numeric);
  components.month = Some(DateTimeFormat::Month::Numeric);
  components.day = Some(DateTimeFormat::Numeric::Numeric);

  components.hour = Some(DateTimeFormat::Numeric::Numeric);
  components.minute = Some(DateTimeFormat::Numeric::TwoDigit);
  components.second = Some(DateTimeFormat::Numeric::TwoDigit);

  TestBuffer<char16_t> buffer;
  ASSERT_TRUE(FormatComponents(buffer, components, MakeStringSpan("es-ES")));
  ASSERT_TRUE(buffer.verboseMatches(u"23/9/2002, 20:07:30"));
}

TEST(IntlDateTimeFormat, ComponentsAll)
{
  // Use most all of the components.
  DateTimeFormat::ComponentsBag components{};

  components.era = Some(DateTimeFormat::Text::Short);

  components.year = Some(DateTimeFormat::Numeric::Numeric);
  components.month = Some(DateTimeFormat::Month::Numeric);
  components.day = Some(DateTimeFormat::Numeric::Numeric);

  components.weekday = Some(DateTimeFormat::Text::Short);

  components.hour = Some(DateTimeFormat::Numeric::Numeric);
  components.minute = Some(DateTimeFormat::Numeric::TwoDigit);
  components.second = Some(DateTimeFormat::Numeric::TwoDigit);

  components.timeZoneName = Some(DateTimeFormat::TimeZoneName::Short);
  components.hourCycle = Some(DateTimeFormat::HourCycle::H24);
  components.fractionalSecondDigits = Some(3);

  TestBuffer<char16_t> buffer;
  ASSERT_TRUE(FormatComponents(buffer, components));
  ASSERT_TRUE(buffer.verboseMatches(u"Mon, 9/23/2002 AD, 20:07:30.000 GMT+3"));
}

TEST(IntlDateTimeFormat, ComponentsHour12Default)
{
  // Assert the behavior of the default "en-US" 12 hour time with day period.
  DateTimeFormat::ComponentsBag components{};
  components.hour = Some(DateTimeFormat::Numeric::Numeric);
  components.minute = Some(DateTimeFormat::Numeric::Numeric);

  TestBuffer<char16_t> buffer;
  ASSERT_TRUE(FormatComponents(buffer, components));
  ASSERT_TRUE(buffer.verboseMatches(u"8:07 PM"));
}

TEST(IntlDateTimeFormat, ComponentsHour24)
{
  // Test the behavior of using 24 hour time to override the default of
  // hour 12 with a day period.
  DateTimeFormat::ComponentsBag components{};
  components.hour = Some(DateTimeFormat::Numeric::Numeric);
  components.minute = Some(DateTimeFormat::Numeric::Numeric);
  components.hour12 = Some(false);

  TestBuffer<char16_t> buffer;
  ASSERT_TRUE(FormatComponents(buffer, components));
  ASSERT_TRUE(buffer.verboseMatches(u"20:07"));
}

TEST(IntlDateTimeFormat, ComponentsHour12DayPeriod)
{
  // Test the behavior of specifying a specific day period.
  DateTimeFormat::ComponentsBag components{};

  components.hour = Some(DateTimeFormat::Numeric::Numeric);
  components.minute = Some(DateTimeFormat::Numeric::Numeric);
  components.dayPeriod = Some(DateTimeFormat::Text::Long);

  TestBuffer<char16_t> buffer;
  ASSERT_TRUE(FormatComponents(buffer, components));
  ASSERT_TRUE(buffer.verboseMatches(u"8:07 in the evening"));
}

static const char* ToString(uint8_t b) { return "uint8_t"; }
static const char* ToString(bool b) { return b ? "true" : "false"; }

static const char* ToString(DateTimeFormat::TimeZoneName aTimeZoneName) {
  using TimeZoneName = DateTimeFormat::TimeZoneName;
  switch (aTimeZoneName) {
    case TimeZoneName::Long:
      return "long";
    case TimeZoneName::Short:
      return "short";
    case TimeZoneName::ShortOffset:
      return "shortOffset";
    case TimeZoneName::LongOffset:
      return "longOffset";
    case TimeZoneName::ShortGeneric:
      return "shortGeneric";
    case TimeZoneName::LongGeneric:
      return "longGeneric";
  }
  MOZ_CRASH("Unexpected DateTimeFormat::TimeZoneName");
}

static const char* ToString(DateTimeFormat::Month aMonth) {
  using Month = DateTimeFormat::Month;
  switch (aMonth) {
    case Month::Numeric:
      return "numeric";
    case Month::TwoDigit:
      return "2-digit";
    case Month::Long:
      return "long";
    case Month::Short:
      return "short";
    case Month::Narrow:
      return "narrow";
  }
  MOZ_CRASH("Unexpected DateTimeFormat::Month");
}

static const char* ToString(DateTimeFormat::Text aText) {
  using Text = DateTimeFormat::Text;
  switch (aText) {
    case Text::Long:
      return "long";
    case Text::Short:
      return "short";
    case Text::Narrow:
      return "narrow";
  }
  MOZ_CRASH("Unexpected DateTimeFormat::Text");
}

static const char* ToString(DateTimeFormat::Numeric aNumeric) {
  using Numeric = DateTimeFormat::Numeric;
  switch (aNumeric) {
    case Numeric::Numeric:
      return "numeric";
    case Numeric::TwoDigit:
      return "2-digit";
  }
  MOZ_CRASH("Unexpected DateTimeFormat::Numeric");
}

static const char* ToString(DateTimeFormat::HourCycle aHourCycle) {
  using HourCycle = DateTimeFormat::HourCycle;
  switch (aHourCycle) {
    case HourCycle::H11:
      return "h11";
    case HourCycle::H12:
      return "h12";
    case HourCycle::H23:
      return "h23";
    case HourCycle::H24:
      return "h24";
  }
  MOZ_CRASH("Unexpected DateTimeFormat::HourCycle");
}

template <typename T>
const char* ToString(Maybe<T> option) {
  if (option) {
    return ToString(*option);
  }
  return "Nothing";
}

template <typename T>
[[nodiscard]] bool VerboseEquals(T expected, T actual, const char* msg) {
  if (expected != actual) {
    fprintf(stderr, "%s\n  Actual: %s\nExpected: %s\n", msg, ToString(actual),
            ToString(expected));
    return false;
  }
  return true;
}

// A testing utility for getting nice errors when ComponentsBags don't match.
[[nodiscard]] bool VerboseEquals(DateTimeFormat::ComponentsBag& expected,
                                 DateTimeFormat::ComponentsBag& actual) {
  // clang-format off
  return
      VerboseEquals(expected.era, actual.era, "Components do not match: bag.era") &&
      VerboseEquals(expected.year, actual.year, "Components do not match: bag.year") &&
      VerboseEquals(expected.month, actual.month, "Components do not match: bag.month") &&
      VerboseEquals(expected.day, actual.day, "Components do not match: bag.day") &&
      VerboseEquals(expected.weekday, actual.weekday, "Components do not match: bag.weekday") &&
      VerboseEquals(expected.hour, actual.hour, "Components do not match: bag.hour") &&
      VerboseEquals(expected.minute, actual.minute, "Components do not match: bag.minute") &&
      VerboseEquals(expected.second, actual.second, "Components do not match: bag.second") &&
      VerboseEquals(expected.timeZoneName, actual.timeZoneName, "Components do not match: bag.timeZoneName") &&
      VerboseEquals(expected.hour12, actual.hour12, "Components do not match: bag.hour12") &&
      VerboseEquals(expected.hourCycle, actual.hourCycle, "Components do not match: bag.hourCycle") &&
      VerboseEquals(expected.dayPeriod, actual.dayPeriod, "Components do not match: bag.dayPeriod") &&
      VerboseEquals(expected.fractionalSecondDigits, actual.fractionalSecondDigits, "Components do not match: bag.fractionalSecondDigits");
  // clang-format on
}

// A utility function to help test the DateTimeFormat::ComponentsBag.
[[nodiscard]] bool ResolveComponentsBag(
    DateTimeFormat::ComponentsBag& aComponentsIn,
    DateTimeFormat::ComponentsBag* aComponentsOut,
    Span<const char> aLocale = MakeStringSpan("en-US")) {
  UniquePtr<DateTimePatternGenerator> gen = nullptr;
  auto dateTimePatternGenerator =
      DateTimePatternGenerator::TryCreate("en").unwrap();
  auto dtFormat = DateTimeFormat::TryCreateFromComponents(
      aLocale, aComponentsIn, dateTimePatternGenerator.get(),
      Some(MakeStringSpan(u"GMT+3")));
  if (dtFormat.isErr()) {
    fprintf(stderr, "Could not create a DateTimeFormat\n");
    return false;
  }

  auto result = dtFormat.unwrap()->ResolveComponents();
  if (result.isErr()) {
    fprintf(stderr, "Could not resolve the components\n");
    return false;
  }

  *aComponentsOut = result.unwrap();
  return true;
}

TEST(IntlDateTimeFormat, ResolvedComponentsDate)
{
  DateTimeFormat::ComponentsBag input{};
  {
    input.year = Some(DateTimeFormat::Numeric::Numeric);
    input.month = Some(DateTimeFormat::Month::Numeric);
    input.day = Some(DateTimeFormat::Numeric::Numeric);
  }

  DateTimeFormat::ComponentsBag expected = input;

  DateTimeFormat::ComponentsBag resolved{};
  ASSERT_TRUE(ResolveComponentsBag(input, &resolved));
  ASSERT_TRUE(VerboseEquals(expected, resolved));
}

TEST(IntlDateTimeFormat, ResolvedComponentsAll)
{
  DateTimeFormat::ComponentsBag input{};
  {
    input.era = Some(DateTimeFormat::Text::Short);

    input.year = Some(DateTimeFormat::Numeric::Numeric);
    input.month = Some(DateTimeFormat::Month::Numeric);
    input.day = Some(DateTimeFormat::Numeric::Numeric);

    input.weekday = Some(DateTimeFormat::Text::Short);

    input.hour = Some(DateTimeFormat::Numeric::Numeric);
    input.minute = Some(DateTimeFormat::Numeric::TwoDigit);
    input.second = Some(DateTimeFormat::Numeric::TwoDigit);

    input.timeZoneName = Some(DateTimeFormat::TimeZoneName::Short);
    input.hourCycle = Some(DateTimeFormat::HourCycle::H24);
    input.fractionalSecondDigits = Some(3);
  }

  DateTimeFormat::ComponentsBag expected = input;
  {
    expected.hour = Some(DateTimeFormat::Numeric::TwoDigit);
    expected.hourCycle = Some(DateTimeFormat::HourCycle::H24);
    expected.hour12 = Some(false);
  }

  DateTimeFormat::ComponentsBag resolved{};
  ASSERT_TRUE(ResolveComponentsBag(input, &resolved));
  ASSERT_TRUE(VerboseEquals(expected, resolved));
}

TEST(IntlDateTimeFormat, ResolvedComponentsHourDayPeriod)
{
  DateTimeFormat::ComponentsBag input{};
  {
    input.hour = Some(DateTimeFormat::Numeric::Numeric);
    input.minute = Some(DateTimeFormat::Numeric::Numeric);
  }

  DateTimeFormat::ComponentsBag expected = input;
  {
    expected.minute = Some(DateTimeFormat::Numeric::TwoDigit);
    expected.hourCycle = Some(DateTimeFormat::HourCycle::H12);
    expected.hour12 = Some(true);
  }

  DateTimeFormat::ComponentsBag resolved{};
  ASSERT_TRUE(ResolveComponentsBag(input, &resolved));
  ASSERT_TRUE(VerboseEquals(expected, resolved));
}

TEST(IntlDateTimeFormat, ResolvedComponentsHour12)
{
  DateTimeFormat::ComponentsBag input{};
  {
    input.hour = Some(DateTimeFormat::Numeric::Numeric);
    input.minute = Some(DateTimeFormat::Numeric::Numeric);
    input.hour12 = Some(false);
  }

  DateTimeFormat::ComponentsBag expected = input;
  {
    expected.hour = Some(DateTimeFormat::Numeric::TwoDigit);
    expected.minute = Some(DateTimeFormat::Numeric::TwoDigit);
    expected.hourCycle = Some(DateTimeFormat::HourCycle::H23);
    expected.hour12 = Some(false);
  }

  DateTimeFormat::ComponentsBag resolved{};
  ASSERT_TRUE(ResolveComponentsBag(input, &resolved));
  ASSERT_TRUE(VerboseEquals(expected, resolved));
}

TEST(IntlDateTimeFormat, GetOriginalSkeleton)
{
  // Demonstrate that the original skeleton and the resolved skeleton can
  // differ.
  DateTimeFormat::ComponentsBag components{};
  components.month = Some(DateTimeFormat::Month::Narrow);
  components.day = Some(DateTimeFormat::Numeric::TwoDigit);

  const char* locale = "zh-Hans-CN";
  auto dateTimePatternGenerator =
      DateTimePatternGenerator::TryCreate(locale).unwrap();

  auto result = DateTimeFormat::TryCreateFromComponents(
      MakeStringSpan(locale), components, dateTimePatternGenerator.get(),
      Some(MakeStringSpan(u"GMT+3")));
  ASSERT_TRUE(result.isOk());
  auto dtFormat = result.unwrap();

  TestBuffer<char16_t> originalSkeleton;
  auto originalSkeletonResult = dtFormat->GetOriginalSkeleton(originalSkeleton);
  ASSERT_TRUE(originalSkeletonResult.isOk());
  ASSERT_TRUE(originalSkeleton.verboseMatches(u"MMMMMdd"));

  TestBuffer<char16_t> pattern;
  auto patternResult = dtFormat->GetPattern(pattern);
  ASSERT_TRUE(patternResult.isOk());
  ASSERT_TRUE(pattern.verboseMatches(u"M月dd日"));

  TestBuffer<char16_t> resolvedSkeleton;
  auto resolvedSkeletonResult = DateTimePatternGenerator::GetSkeleton(
      Span(pattern.data(), pattern.length()), resolvedSkeleton);

  ASSERT_TRUE(resolvedSkeletonResult.isOk());
  ASSERT_TRUE(resolvedSkeleton.verboseMatches(u"Mdd"));
}

TEST(IntlDateTimeFormat, GetAvailableLocales)
{
  int32_t english = 0;
  int32_t german = 0;
  int32_t chinese = 0;

  // Since this list is dependent on ICU, and may change between upgrades, only
  // test a subset of the available locales.
  for (mozilla::Span<const char> locale :
       DateTimeFormat::GetAvailableLocales()) {
    if (locale == mozilla::MakeStringSpan("en")) {
      english++;
    } else if (locale == mozilla::MakeStringSpan("de")) {
      german++;
    } else if (locale == mozilla::MakeStringSpan("zh")) {
      chinese++;
    }
  }

  // Each locale should be found exactly once.
  ASSERT_EQ(english, 1);
  ASSERT_EQ(german, 1);
  ASSERT_EQ(chinese, 1);
}

TEST(IntlDateTimeFormat, TryFormatToParts)
{
  auto dateTimePatternGenerator =
      DateTimePatternGenerator::TryCreate("en").unwrap();

  DateTimeFormat::ComponentsBag components;
  components.year = Some(DateTimeFormat::Numeric::Numeric);
  components.month = Some(DateTimeFormat::Month::TwoDigit);
  components.day = Some(DateTimeFormat::Numeric::TwoDigit);
  components.hour = Some(DateTimeFormat::Numeric::TwoDigit);
  components.minute = Some(DateTimeFormat::Numeric::TwoDigit);
  components.hour12 = Some(false);

  UniquePtr<DateTimeFormat> dtFormat =
      DateTimeFormat::TryCreateFromComponents(
          MakeStringSpan("en-US"), components, dateTimePatternGenerator.get(),
          Some(MakeStringSpan(u"GMT")))
          .unwrap();

  TestBuffer<char16_t> buffer;
  mozilla::intl::DateTimePartVector parts;
  auto result = dtFormat->TryFormatToParts(DATE, buffer, parts);
  ASSERT_TRUE(result.isOk());

  std::u16string_view strView = buffer.get_string_view();
  ASSERT_EQ(strView, u"09/23/2002, 17:07");

  auto getSubStringView = [strView, &parts](size_t index) {
    size_t pos = index == 0 ? 0 : parts[index - 1].mEndIndex;
    size_t count = parts[index].mEndIndex - pos;
    return strView.substr(pos, count);
  };

  ASSERT_EQ(parts[0].mType, DateTimePartType::Month);
  ASSERT_EQ(getSubStringView(0), u"09");

  ASSERT_EQ(parts[1].mType, DateTimePartType::Literal);
  ASSERT_EQ(getSubStringView(1), u"/");

  ASSERT_EQ(parts[2].mType, DateTimePartType::Day);
  ASSERT_EQ(getSubStringView(2), u"23");

  ASSERT_EQ(parts[3].mType, DateTimePartType::Literal);
  ASSERT_EQ(getSubStringView(3), u"/");

  ASSERT_EQ(parts[4].mType, DateTimePartType::Year);
  ASSERT_EQ(getSubStringView(4), u"2002");

  ASSERT_EQ(parts[5].mType, DateTimePartType::Literal);
  ASSERT_EQ(getSubStringView(5), u", ");

  ASSERT_EQ(parts[6].mType, DateTimePartType::Hour);
  ASSERT_EQ(getSubStringView(6), u"17");

  ASSERT_EQ(parts[7].mType, DateTimePartType::Literal);
  ASSERT_EQ(getSubStringView(7), u":");

  ASSERT_EQ(parts[8].mType, DateTimePartType::Minute);
  ASSERT_EQ(getSubStringView(8), u"07");

  ASSERT_EQ(parts.length(), 9u);
}

TEST(IntlDateTimeFormat, SetStartTimeIfGregorian)
{
  using namespace std::literals;

  DateTimeFormat::StyleBag style{};
  style.date = Some(DateTimeFormat::Style::Long);

  auto timeZone = Some(MakeStringSpan(u"UTC"));

  // Gregorian change date defaults to October 15, 1582 in ICU. Test with a date
  // before the default change date, in this case January 1, 1582.
  constexpr double FirstJanuary1582 = -12244089600000.0;

  // One year expressed in milliseconds.
  constexpr double oneYear = (365 * 24 * 60 * 60) * 1000.0;

  // Test with and without explicit calendar. The start time of the calendar can
  // only be adjusted for the Gregorian and the ISO-8601 calendar.
  for (const char* locale : {
           "en-US",
           "en-US-u-ca-gregory",
           "en-US-u-ca-iso8601",
       }) {
    auto gen = DateTimePatternGenerator::TryCreate(locale).unwrap();

    auto dtFormat = DateTimeFormat::TryCreateFromStyle(
                        MakeStringSpan(locale), style, gen.get(), timeZone)
                        .unwrap();

    const char* Jan01_1582;
    const char* Jan01_1583;
    if (locale == "en-US-u-ca-iso8601"sv) {
      Jan01_1582 = "1582 January 1";
      Jan01_1583 = "1583 January 1";
    } else {
      Jan01_1582 = "January 1, 1582";
      Jan01_1583 = "January 1, 1583";
    }

    TestBuffer<char> buffer;

    // Before the default Gregorian change date, but not interpreted in the
    // Julian calendar, which is December 22, 1581. Instead interpreted in
    // proleptic Gregorian calendar at January 1, 1582.
    dtFormat->TryFormat(FirstJanuary1582, buffer).unwrap();
    ASSERT_TRUE(buffer.verboseMatches(Jan01_1582));

    // After default Gregorian change date, so January 1, 1583.
    dtFormat->TryFormat(FirstJanuary1582 + oneYear, buffer).unwrap();
    ASSERT_TRUE(buffer.verboseMatches(Jan01_1583));
  }
}

TEST(IntlDateTimeFormat, GetTimeSeparator)
{
  struct TestData {
    const char* locale;
    const char* numberingSystem;
    const char16_t* expected;
  } testData[] = {
      {"root", "latn", u":"},
      {"root", "arab", u":"},
      {"root", "thai", u":"},
      {"root", "arabext", u"٫"},

      // English uses the same data as the root locale.
      {"en", "latn", u":"},
      {"en", "arab", u":"},
      {"en", "thai", u":"},
      {"en", "arabext", u"٫"},

      // Spanish uses the same data as the root locale.
      {"es", "latn", u":"},
      {"es", "arab", u":"},
      {"es", "thai", u":"},
      {"es", "arabext", u"٫"},

      // German (Austria) uses the same data as the root locale.
      {"de-AT", "latn", u":"},
      {"de-AT", "arab", u":"},
      {"de-AT", "thai", u":"},
      {"de-AT", "arabext", u"٫"},

      // Danish has a different time separator for "latn".
      {"da", "latn", u"."},
      {"da", "arab", u":"},
      {"da", "thai", u"."},
      {"da", "arabext", u"٫"},

      // Same time separator as Danish.
      {"en-DK", "latn", u"."},
      {"en-DK", "arab", u":"},
      {"en-DK", "thai", u"."},
      {"en-DK", "arabext", u"٫"},

      // Norwegian overrides time separators for "arab" and "arabext".
      {"no", "latn", u":"},
      {"no", "arab", u"."},
      {"no", "thai", u":"},
      {"no", "arabext", u"."},

      // Parent locale of Bokmål is Norwegian.
      {"nb", "latn", u":"},
      {"nb", "arab", u"."},
      {"nb", "thai", u":"},
      {"nb", "arabext", u"."},

      // Farsi overrides the time separator for "arabext".
      {"fa", "latn", u":"},
      {"fa", "arab", u":"},
      {"fa", "thai", u":"},
      {"fa", "arabext", u":"},
  };

  for (const auto& data : testData) {
    TestBuffer<char16_t> timeSeparator;
    auto timeSeparatorResult = DateTimeFormat::GetTimeSeparator(
        MakeStringSpan(data.locale), MakeStringSpan(data.numberingSystem),
        timeSeparator);
    ASSERT_TRUE(timeSeparatorResult.isOk());
    ASSERT_TRUE(timeSeparator.verboseMatches(data.expected));
  }
}
}  // namespace mozilla::intl
