// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// Tests for the Locale class.
///
/// Currently, the primary intention of these tests is to exercise and
/// demonstrate the API: full test coverage is a non-goal for the prototype.
///
/// For production code, use of ICU would influence what needs and doesn't need
/// to be tested.
import 'package:intl/locale.dart';
import 'package:test/test.dart';

import 'locale_test_data.dart';

void main() {
  group('Construction and properties:', () {
    // Simple with normalization:
    testFromSubtags('Zh', null, null, 'zh', null, null, 'zh');
    testFromSubtags('zH', null, 'cn', 'zh', null, 'CN', 'zh-CN');
    testFromSubtags('ZH', null, 'Cn', 'zh', null, 'CN', 'zh-CN');
    testFromSubtags('zh', null, 'cN', 'zh', null, 'CN', 'zh-CN');
    testFromSubtags('zh', 'hans', null, 'zh', 'Hans', null, 'zh-Hans');
    testFromSubtags('ZH', 'HANS', 'CN', 'zh', 'Hans', 'CN', 'zh-Hans-CN');

    // Region codes can be three digits.
    testFromSubtags('es', null, '419', 'es', null, '419', 'es-419');

    // While language is usually 2 characters, it can also be 3.
    testFromSubtags('CKB', 'arab', null, 'ckb', 'Arab', null, 'ckb-Arab');

    // With canonicalization:
    testFromSubtags('Iw', null, null, 'he', null, null, 'he');
    testFromSubtags('iW', null, null, 'he', null, null, 'he');
    testFromSubtags('My', null, 'Bu', 'my', null, 'MM', 'my-MM');
  });

  group('Locale.fromSubtags() FormatExceptions:', () {
    void testExceptionForSubtags(
        String language, String? script, String? region) {
      test('fromSubtags: "$language / $script / $region"', () {
        expect(
            () => Locale.fromSubtags(
                languageCode: language,
                scriptCode: script,
                countryCode: region),
            throwsFormatException);
      });
    }

    testExceptionForSubtags('a', null, null);
    testExceptionForSubtags('en', 'ZA', null);
    testExceptionForSubtags('en', null, 'Latn');
  });

  group('Locale normalization matching ICU.', () {
    localeParsingTestData.forEach((unnormalized, normalized) {
      test('Locale normalization: $unnormalized -> $normalized', () {
        expect(Locale.parse(unnormalized).toLanguageTag(), normalized);
      });
    });
  });

  group('Unicode LDML Locale Identifier support', () {
    // 'root' is a valid Unicode Locale Identifier, but should be taken as
    // 'und'[1]. ICU's toLanguageTag still returns 'root'.
    // [1]:
    // http://unicode.org/reports/tr35/#Unicode_Locale_Identifier_CLDR_to_BCP_47
    testParse('root', 'und', null, null, [], 'und');
    testParse('Root', 'und', null, null, [], 'und');
    testParse('ROOT', 'und', null, null, [], 'und');

    // We support underscores, whereas ICU's `forLanguageTag` does
    // not.
    testParse('CKB_arab', 'ckb', 'Arab', null, [], 'ckb-Arab');
    testParse('My_Bu', 'my', null, 'MM', [], 'my-MM');

    // Normalises tags, sorts subtags alphabetically, including variants[1]:
    // ICU is currently not sorting variants.
    // [1]: http://unicode.org/reports/tr35/#Unicode_locale_identifier
    testParse('en-scouse-fonipa', 'en', null, null, ['fonipa', 'scouse'],
        'en-fonipa-scouse');

    // Normalises tags, sorts subtags alphabetically and suppresses unneeded
    // "true" in u extension (ICU is currently not dropping -true):
    // http://unicode.org/reports/tr35/#u_Extension
    testParse('en-u-Foo-bar-nu-thai-ca-buddhist-kk-true', 'en', null, null, [],
        'en-u-bar-foo-ca-buddhist-kk-nu-thai');

    // The specification does permit empty extensions for extensions other than
    // u- and t-.
    testParse('en-a', 'en', null, null, [], 'en');
    testParse('en-x', 'en', null, null, [], 'en');
    testParse('en-z', 'en', null, null, [], 'en');

    // Normalization of `tlang` - ICU still returns -t-iw-bu.
    testParse('en-t-iw-Bu', 'en', null, null, [], 'en-t-he-mm');

    test('en-u-ca is equivalent to en-u-ca-true', () {
      expect(Locale.parse('en-u-ca').toLanguageTag(),
          Locale.parse('en-u-ca-true').toLanguageTag());
    });
  });

  // Normalization: sorting of extension subtags:
  testParse('en-z-abc-001-foo-fii-bar-u-cu-usd-co-phonebk', 'en', null, null,
      [], 'en-u-co-phonebk-cu-usd-z-abc-001-foo-fii-bar');

  group('Locale.parse() throws FormatException:', () {
    void testExceptionForId(String x) {
      test('"$x"', () {
        expect(() => Locale.parse(x), throwsFormatException);
      });
    }

    for (var badLocaleIdentifier in invalidLocales) {
      testExceptionForId(badLocaleIdentifier);
    }

    // ICU permits '', taking it as 'und', but it is not a valid Unicode Locale
    // Identifier: We reject it.
    testExceptionForId('');

    // abcd-Latn throws exceptions in our Dart implementation, whereas
    // ECMAScript's Intl.Locale permits it. This is because the BCP47 spec
    // still allows for the possible addition of 4-character languages in
    // the future, whereas the Unicode Locale Identifiers spec bans it
    // outright.
    testExceptionForId('abcd-Latn');

    // ICU permits 'root-Latn' since it conforms to pure BCP47, but it is an
    // invalid Unicode BCP47 Locale Identifier.
    testExceptionForId('root-Latn');

    // ICU permits empty tkeys.
    testExceptionForId('en-t-a0');

    // ICU permits duplicate tkeys, returning the content of -t- verbatim.
    testExceptionForId('en-t-a0-one-a0-two');

    // ICU permits duplicate keys, in this case dropping -ca-buddhist.
    testExceptionForId('en-u-ca-islamic-ca-buddhist');
  });

  group('Locale.tryParse() returns null:', () {
    for (var badLocaleIdentifier in invalidLocales) {
      test('"$badLocaleIdentifier"', () {
        expect(Locale.tryParse(badLocaleIdentifier), isNull);
      });
    }
  });

  // TODO: determine appropriate behaviour for the following examples.

  // // 'mo' is deprecated, and is a tag that ought to be replaced by *two*
  // // subtags (ro-MD), although Chrome Unstable also doesn't presently do
  // // that (replaces it by 'ro' only).
  // // TODO: check up on the Chrome implementation.
  // testParse('mo', 'ro', null, 'MD', [], 'ro-MD');

  // // Script deprecation.
  // testParse('en-Qaai', 'en', 'Zinh', null, [], 'en-Zinh');

  // // Variant deprecation.
  // testParse('sv-aaland', 'sv', null, 'AX', [], 'sv-AX');

  // // Variant deprecation.
  // testParse('en-heploc', 'en', null, null, ['alalc97'], 'en-alalc97');

  // // Variant deprecation.
  // testParse('en-polytoni', 'en', null, null, ['polyton'], 'en-polyton');

  test('Locale cannot be modified via the variants field', () {
    var l = Locale.parse('en-scotland');
    var v = l.variants as List<String>;
    var good = false;
    try {
      v.add('basiceng');
    } on Error {
      good = true;
    }
    expect(l.toLanguageTag(), 'en-scotland');
    expect(good, isTrue);
  });

  test('operator== and hashCode', () {
    Locale l1, l2;

    l1 = Locale.parse('en-Shaw-ZA');
    l2 = Locale.fromSubtags(
        languageCode: 'en', scriptCode: 'Shaw', countryCode: 'ZA');
    expect(l1, l2);
    expect(l1.hashCode, l2.hashCode);

    l1 = Locale.parse('en');
    l2 = Locale.fromSubtags(
        languageCode: 'en', scriptCode: null, countryCode: null);
    expect(l1, l2);
    expect(l1.hashCode, l2.hashCode);
  });
}

void testFromSubtags(
    String language,
    String? script,
    String? region,
    String? expectedLanguage,
    String? expectedScript,
    String? expectedRegion,
    String? expectedTag) {
  test('Locale.fromSubtags(...) with $language, $script, $region', () {
    var l = Locale.fromSubtags(
        languageCode: language, scriptCode: script, countryCode: region);
    expect(l.languageCode, expectedLanguage);
    expect(l.scriptCode, expectedScript);
    expect(l.countryCode, expectedRegion);
    expect(l.toLanguageTag(), expectedTag);
    expect(l.toString(), expectedTag);
  });
}

void testParse(
    String bcp47Tag,
    String expectedLanguage,
    String? expectedScript,
    String? expectedRegion,
    Iterable<String> expectedVariants,
    String? expectedTag) {
  test('Locale.parse("$bcp47Tag");', () {
    var l = Locale.parse(bcp47Tag);
    expect(l.languageCode, expectedLanguage);
    expect(l.scriptCode, expectedScript);
    expect(l.countryCode, expectedRegion);
    expect(l.toLanguageTag(), expectedTag);
    expect(l.variants, orderedEquals(expectedVariants));
  });
}
