// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef V8_INTL_SUPPORT

#include "src/objects/intl-objects.h"
#include "src/objects/js-break-iterator.h"
#include "src/objects/js-collator-inl.h"
#include "src/objects/js-date-time-format.h"
#include "src/objects/js-list-format.h"
#include "src/objects/js-number-format.h"
#include "src/objects/js-plural-rules.h"
#include "src/objects/js-relative-time-format.h"
#include "src/objects/js-segmenter.h"
#include "src/objects/lookup.h"
#include "src/objects/objects-inl.h"
#include "src/objects/option-utils.h"
#include "test/unittests/test-utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "unicode/coll.h"

namespace v8 {
namespace internal {

using IntlTest = TestWithContext;

// This operator overloading enables CHECK_EQ to be used with
// std::vector<NumberFormatSpan>
bool operator==(const NumberFormatSpan& lhs, const NumberFormatSpan& rhs) {
  return memcmp(&lhs, &rhs, sizeof(lhs)) == 0;
}
template <typename _CharT, typename _Traits>
std::basic_ostream<_CharT, _Traits>& operator<<(
    std::basic_ostream<_CharT, _Traits>& self, const NumberFormatSpan& part) {
  return self << "{" << part.field_id << "," << part.begin_pos << ","
              << part.end_pos << "}";
}

void test_flatten_regions_to_parts(
    const std::vector<NumberFormatSpan>& regions,
    const std::vector<NumberFormatSpan>& expected_parts) {
  std::vector<NumberFormatSpan> mutable_regions = regions;
  std::vector<NumberFormatSpan> parts = FlattenRegionsToParts(&mutable_regions);
  CHECK_EQ(expected_parts, parts);
}

TEST_F(IntlTest, FlattenRegionsToParts) {
  test_flatten_regions_to_parts(
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(-1, 0, 10),
          NumberFormatSpan(1, 2, 8),
          NumberFormatSpan(2, 2, 4),
          NumberFormatSpan(3, 6, 8),
      },
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(-1, 0, 2),
          NumberFormatSpan(2, 2, 4),
          NumberFormatSpan(1, 4, 6),
          NumberFormatSpan(3, 6, 8),
          NumberFormatSpan(-1, 8, 10),
      });
  test_flatten_regions_to_parts(
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(0, 0, 1),
      },
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(0, 0, 1),
      });
  test_flatten_regions_to_parts(
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(-1, 0, 1),
          NumberFormatSpan(0, 0, 1),
      },
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(0, 0, 1),
      });
  test_flatten_regions_to_parts(
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(0, 0, 1),
          NumberFormatSpan(-1, 0, 1),
      },
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(0, 0, 1),
      });
  test_flatten_regions_to_parts(
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(-1, 0, 10),
          NumberFormatSpan(1, 0, 1),
          NumberFormatSpan(2, 0, 2),
          NumberFormatSpan(3, 0, 3),
          NumberFormatSpan(4, 0, 4),
          NumberFormatSpan(5, 0, 5),
          NumberFormatSpan(15, 5, 10),
          NumberFormatSpan(16, 6, 10),
          NumberFormatSpan(17, 7, 10),
          NumberFormatSpan(18, 8, 10),
          NumberFormatSpan(19, 9, 10),
      },
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(1, 0, 1),
          NumberFormatSpan(2, 1, 2),
          NumberFormatSpan(3, 2, 3),
          NumberFormatSpan(4, 3, 4),
          NumberFormatSpan(5, 4, 5),
          NumberFormatSpan(15, 5, 6),
          NumberFormatSpan(16, 6, 7),
          NumberFormatSpan(17, 7, 8),
          NumberFormatSpan(18, 8, 9),
          NumberFormatSpan(19, 9, 10),
      });

  //              :          4
  //              :      22 33    3
  //              :      11111   22
  // input regions:     0000000  111
  //              :     ------------
  // output parts:      0221340--231
  test_flatten_regions_to_parts(
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(-1, 0, 12),
          NumberFormatSpan(0, 0, 7),
          NumberFormatSpan(1, 9, 12),
          NumberFormatSpan(1, 1, 6),
          NumberFormatSpan(2, 9, 11),
          NumberFormatSpan(2, 1, 3),
          NumberFormatSpan(3, 10, 11),
          NumberFormatSpan(3, 4, 6),
          NumberFormatSpan(4, 5, 6),
      },
      std::vector<NumberFormatSpan>{
          NumberFormatSpan(0, 0, 1),
          NumberFormatSpan(2, 1, 3),
          NumberFormatSpan(1, 3, 4),
          NumberFormatSpan(3, 4, 5),
          NumberFormatSpan(4, 5, 6),
          NumberFormatSpan(0, 6, 7),
          NumberFormatSpan(-1, 7, 9),
          NumberFormatSpan(2, 9, 10),
          NumberFormatSpan(3, 10, 11),
          NumberFormatSpan(1, 11, 12),
      });
}

TEST_F(IntlTest, GetStringOption) {
  Handle<JSObject> options = i_isolate()->factory()->NewJSObjectWithNullProto();
  {
    // No value found
    std::unique_ptr<char[]> result = nullptr;
    Maybe<bool> found =
        GetStringOption(i_isolate(), options, "foo", std::vector<const char*>{},
                        "service", &result);
    CHECK(!found.FromJust());
    CHECK_NULL(result);
  }

  Handle<String> key = i_isolate()->factory()->NewStringFromAsciiChecked("foo");
  LookupIterator it(i_isolate(), options, key);
  CHECK(Object::SetProperty(&it, Handle<Smi>(Smi::FromInt(42), i_isolate()),
                            StoreOrigin::kMaybeKeyed,
                            Just(ShouldThrow::kThrowOnError))
            .FromJust());

  {
    // Value found
    std::unique_ptr<char[]> result = nullptr;
    Maybe<bool> found =
        GetStringOption(i_isolate(), options, "foo", std::vector<const char*>{},
                        "service", &result);
    CHECK(found.FromJust());
    CHECK_NOT_NULL(result);
    CHECK_EQ(0, strcmp("42", result.get()));
  }

  {
    // No expected value in values array
    std::unique_ptr<char[]> result = nullptr;
    Maybe<bool> found =
        GetStringOption(i_isolate(), options, "foo",
                        std::vector<const char*>{"bar"}, "service", &result);
    CHECK(i_isolate()->has_pending_exception());
    CHECK(found.IsNothing());
    CHECK_NULL(result);
    i_isolate()->clear_pending_exception();
  }

  {
    // Expected value in values array
    std::unique_ptr<char[]> result = nullptr;
    Maybe<bool> found =
        GetStringOption(i_isolate(), options, "foo",
                        std::vector<const char*>{"42"}, "service", &result);
    CHECK(found.FromJust());
    CHECK_NOT_NULL(result);
    CHECK_EQ(0, strcmp("42", result.get()));
  }
}

TEST_F(IntlTest, GetBoolOption) {
  Handle<JSObject> options = i_isolate()->factory()->NewJSObjectWithNullProto();
  {
    bool result = false;
    Maybe<bool> found =
        GetBoolOption(i_isolate(), options, "foo", "service", &result);
    CHECK(!found.FromJust());
    CHECK(!result);
  }

  Handle<String> key = i_isolate()->factory()->NewStringFromAsciiChecked("foo");
  {
    LookupIterator it(i_isolate(), options, key);
    Handle<Object> false_value =
        handle(i::ReadOnlyRoots(i_isolate()).false_value(), i_isolate());
    Object::SetProperty(i_isolate(), options, key, false_value,
                        StoreOrigin::kMaybeKeyed,
                        Just(ShouldThrow::kThrowOnError))
        .Assert();
    bool result = false;
    Maybe<bool> found =
        GetBoolOption(i_isolate(), options, "foo", "service", &result);
    CHECK(found.FromJust());
    CHECK(!result);
  }

  {
    LookupIterator it(i_isolate(), options, key);
    Handle<Object> true_value =
        handle(i::ReadOnlyRoots(i_isolate()).true_value(), i_isolate());
    Object::SetProperty(i_isolate(), options, key, true_value,
                        StoreOrigin::kMaybeKeyed,
                        Just(ShouldThrow::kThrowOnError))
        .Assert();
    bool result = false;
    Maybe<bool> found =
        GetBoolOption(i_isolate(), options, "foo", "service", &result);
    CHECK(found.FromJust());
    CHECK(result);
  }
}

TEST_F(IntlTest, GetAvailableLocales) {
  std::set<std::string> locales;

  locales = JSV8BreakIterator::GetAvailableLocales();
  CHECK(locales.count("en-US"));
  CHECK(!locales.count("abcdefg"));

  locales = JSCollator::GetAvailableLocales();
  CHECK(locales.count("en-US"));

  locales = JSDateTimeFormat::GetAvailableLocales();
  CHECK(locales.count("en-US"));

  locales = JSListFormat::GetAvailableLocales();
  CHECK(locales.count("en-US"));

  locales = JSNumberFormat::GetAvailableLocales();
  CHECK(locales.count("en-US"));

  locales = JSPluralRules::GetAvailableLocales();
  CHECK(locales.count("en"));

  locales = JSRelativeTimeFormat::GetAvailableLocales();
  CHECK(locales.count("en-US"));

  locales = JSSegmenter::GetAvailableLocales();
  CHECK(locales.count("en-US"));
  CHECK(!locales.count("abcdefg"));
}

// Tests that the LocaleCompare fast path and generic path return the same
// comparison results for all ASCII strings.
TEST_F(IntlTest, StringLocaleCompareFastPath) {
  // We compare all single-char strings of printable ASCII characters.
  std::vector<Handle<String>> ascii_strings;
  for (int c = 0; c <= 0x7F; c++) {
    if (!std::isprint(c)) continue;
    ascii_strings.push_back(
        i_isolate()->factory()->LookupSingleCharacterStringFromCode(c));
  }

  Handle<JSFunction> collator_constructor = Handle<JSFunction>(
      JSFunction::cast(
          i_isolate()->context().native_context().intl_collator_function()),
      i_isolate());
  Handle<Map> constructor_map =
      JSFunction::GetDerivedMap(i_isolate(), collator_constructor,
                                collator_constructor)
          .ToHandleChecked();
  Handle<Object> options(ReadOnlyRoots(i_isolate()).undefined_value(),
                         i_isolate());
  static const char* const kMethodName = "StringLocaleCompareFastPath";

  // For all fast locales, exhaustively compare within the printable ASCII
  // range.
  const std::set<std::string>& locales = JSCollator::GetAvailableLocales();
  for (const std::string& locale : locales) {
    Handle<String> locale_string =
        i_isolate()->factory()->NewStringFromAsciiChecked(locale.c_str());

    if (Intl::CompareStringsOptionsFor(i_isolate()->AsLocalIsolate(),
                                       locale_string, options) !=
        Intl::CompareStringsOptions::kTryFastPath) {
      continue;
    }

    Handle<JSCollator> collator =
        JSCollator::New(i_isolate(), constructor_map, locale_string, options,
                        kMethodName)
            .ToHandleChecked();

    for (size_t i = 0; i < ascii_strings.size(); i++) {
      Handle<String> lhs = ascii_strings[i];
      for (size_t j = i + 1; j < ascii_strings.size(); j++) {
        Handle<String> rhs = ascii_strings[j];
        CHECK_EQ(
            Intl::CompareStrings(i_isolate(), *collator->icu_collator().raw(),
                                 lhs, rhs, Intl::CompareStringsOptions::kNone),
            Intl::CompareStrings(i_isolate(), *collator->icu_collator().raw(),
                                 lhs, rhs,
                                 Intl::CompareStringsOptions::kTryFastPath));
      }
    }
  }
}

TEST_F(IntlTest, IntlMathematicalValueFromString) {
  struct TestCase {
    bool is_nan;
    bool is_minus_zero;
    bool is_negative;
    bool is_negative_infinity;
    bool is_positive_infinity;
    bool is_mathematical_value;
    const char* string;
  } cases[] = {
      {false, false, false, false, false, true, "+1"},
      {false, false, false, false, false, true,
       "+1234567890123456789012345678901234567890"},
      {false, false, false, false, true, false,
       "+1234567890123456789012345678901234567890e987654321"},
      {false, false, false, false, true, false,
       "    +1234567890123456789012345678901234567890e987654321  "},
      {true, false, false, false, false, false,
       "    +12   345 67  "},  // space between digit is invalid
      {true, false, false, false, false, false,
       "    -12   345 67  "},  // space between digit is invalid
      {false, false, false, false, false, true,
       "1234567890123456789012345678901234567890"},
      {false, false, false, false, false, true,
       "+.1234567890123456789012345678901234567890"},
      {false, false, false, false, false, true,
       ".1234567890123456789012345678901234567890"},
      {false, false, false, false, false, true, ".1234567890123456789e123"},
      {false, false, false, false, false, true, ".1234567890123456789E123"},
      {false, false, false, false, false, true, ".1234567890123456789e+123"},
      {false, false, false, false, false, true, ".1234567890123456789E+123"},
      {false, false, false, false, false, true, ".1234567890123456789e-0123"},
      {false, false, false, false, false, true, ".1234567890123456789E-0123"},
      {false, false, false, false, false, true,
       "1234567890123456789012345678901234567.890"},
      {false, false, false, false, false, true,
       "1234567890123456789012345678901234567890."},
      {true, false, false, false, false, false,
       "1234567.90123456789012345678901234567.890"},  // two '.'
      {true, false, false, false, false, false,
       ".1234567890123456789e12.3"},  // two '.'
      {false, false, true, false, false, true, "-1"},
      {false, false, true, false, false, true, "-1e33  "},
      {false, false, true, false, false, true, "  -0.21e33"},
      {false, false, false, false, false, true, "  0.21e33"},
      {false, true, false, false, false, false, "-0"},
      {false, false, false, false, false, true, "1"},
      {false, false, true, false, false, true, "  -1234.567e-20  "},
      {false, true, false, false, false, false, "  -1234.567e-9876  "},
      {false, false, false, false, true, false, "  Infinity "},
      {false, false, true, true, false, false, "        -Infinity "},
      {true, false, false, false, false, false, "yz"},  // not digits
      {false, false, true, false, false, true,
       "  -12345678901234567890122345.6778901234567890e234 "},
      {false, false, false, false, false, true,
       "  12345678901234567890122345.6778901234567890e-234 "},
      {false, false, false, false, false, true, "  0b01010001 "},
      {false, false, false, false, false, true, "  0B01010001 "},
      {true, false, false, false, false, false,
       "  -0b01010001 "},  // invalid binary becaues of -
      {true, false, false, false, false, false,
       "  -0B01010001 "},  // invalid binary becaues of -
      {true, false, false, false, false, false,
       "  0b01010002 "},  // invalid binary becaues of 2
      {true, false, false, false, false, false,
       "  0B01010003 "},  // invalid binary becaues of 3
      {false, false, false, false, false, true, "  0o01234567 "},
      {false, false, false, false, false, true, "  0O76543210 "},
      {true, false, false, false, false, false,
       "  -0o01234567 "},  // invalid oct becaues of -
      {true, false, false, false, false, false,
       "  -0O76543210 "},  // invalid oct becaues of -
      {true, false, false, false, false, false,
       "  0o012345678 "},  // invalid oct becaues of 8
      {true, false, false, false, false, false,
       "  0O765432108 "},  // invalid oct becaues of 8
      {false, false, false, false, false, true, "  0x123456789aBcDeF "},
      {false, false, false, false, false, true, "  0X123456789AbCdEf "},
      {true, false, false, false, false, false,
       "  -0x123456789aBcDeF "},  // invalid hex because of -
      {true, false, false, false, false, false,
       "  -0X123456789AbCdEf "},  // invalid hex because of -
      {true, false, false, false, false, false,
       "  0x012345678xyz "},  // invalid hex because xyz
      {true, false, false, false, false, false,
       "  0X765432108xyz "},  // invalid hex because xyz
  };
  for (auto& cas : cases) {
    IntlMathematicalValue x =
        IntlMathematicalValue::From(
            i_isolate(),
            i_isolate()->factory()->NewStringFromAsciiChecked(cas.string))
            .ToChecked();
    CHECK_EQ(x.IsNaN(), cas.is_nan);
    CHECK_EQ(x.IsMinusZero(), cas.is_minus_zero);
    CHECK_EQ(x.IsNegative(), cas.is_negative);
    CHECK_EQ(x.IsNegativeInfinity(), cas.is_negative_infinity);
    CHECK_EQ(x.IsPositiveInfinity(), cas.is_positive_infinity);
    CHECK_EQ(x.IsMathematicalValue(), cas.is_mathematical_value);
  }
}

TEST_F(IntlTest, IntlMathematicalValueFromBigInt) {
  struct TestCase {
    bool is_negative;
    const char* bigint_string;
  } cases[] = {
      {false, "12"},
      {false, "12345678901234567890123456789012345678901234567890"},
      {true, "-12345678901234567890123456789012345678901234567890"},
      {false, "0"},
      {true, "-20"},
  };
  for (auto& cas : cases) {
    printf("%s\n", cas.bigint_string);
    Handle<String> str =
        i_isolate()->factory()->NewStringFromAsciiChecked(cas.bigint_string);
    IntlMathematicalValue x =
        IntlMathematicalValue::From(
            i_isolate(), BigInt::FromObject(i_isolate(), str).ToHandleChecked())
            .ToChecked();
    CHECK_EQ(x.IsNaN(), false);
    CHECK_EQ(x.IsMinusZero(), false);
    CHECK_EQ(x.IsNegative(), cas.is_negative);
    CHECK_EQ(x.IsNegativeInfinity(), false);
    CHECK_EQ(x.IsPositiveInfinity(), false);
    CHECK_EQ(x.IsMathematicalValue(), true);
  }
}

TEST_F(IntlTest, IntlMathematicalValueLessThanString) {
  struct TestCase {
    const char* x;
    const char* y;
    bool is_x_less_than_y;
  } cases[] = {
      {" 1 ", " 2", true},
      {" 1 ", "       2 ", true},
      {" 1e-1 ", "       2 ", true},
      {" 1e1 ", "       2 ", false},
      {" 1 ", " 20e-3", false},
      {" -1e10 ", " -1e9 ", true},
      {" -1e-10 ", " -1e-9 ", false},
      {" 123456789012345678901234567890 ", " 123456789012345678901234567890 ",
       false},
      {" .123456789012345678901234567890 ", " .123456789012345678901234567890 ",
       false},
      {" .123456789012345678901234567890000 ",
       " .12345678901234567890123456789 ", false},
      {" .12345678901234567890123456789 ",
       " .123456789012345678901234567890000 ", false},
      {" 123456789012345678901234567890 ", " 1234567890123456789012345678901 ",
       true},
      {" 1234567890123456789012345678902 ", " 1234567890123456789012345678901 ",
       false},
      {" 123456789012345.678901234567890e33 ",
       " 12345678901234.5678901234567890e34 ", false},
      {" 123456789012345.678901234567890e33 ",
       " 12345678901234.5678901234567890e35 ", true},
      {" 12345678901234.5678901234567890e34 ",
       " 123456789012345.678901234567890e33 ", false},
      {" 123456789012345678.901234567890e30 ",
       " 12345678901234.5678901234567890e35 ", true},
      {" .12345678901234567890123456789 ",
       " .1234567890123456789012345678900000001 ", true},
      {" -.1234567890123456789012345678900000001 ",
       " -.123456789012345678901234567890000 ", true},
      {" -.1234567890123456789012345678900000001 ",
       " -0.00000123456789012345678901234567890000e5 ", true},
  };
  for (auto& cas : cases) {
    IntlMathematicalValue x =
        IntlMathematicalValue::From(
            i_isolate(),
            i_isolate()->factory()->NewStringFromAsciiChecked(cas.x))
            .ToChecked();
    IntlMathematicalValue y =
        IntlMathematicalValue::From(
            i_isolate(),
            i_isolate()->factory()->NewStringFromAsciiChecked(cas.y))
            .ToChecked();
    CHECK_EQ(x.IsLessThan(i_isolate(), y), cas.is_x_less_than_y);
  }
}

}  // namespace internal
}  // namespace v8

#endif  // V8_INTL_SUPPORT