// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/localizations/gen_l10n.dart';
import 'package:flutter_tools/src/localizations/gen_l10n_types.dart';
import 'package:flutter_tools/src/localizations/localizations_utils.dart';
import 'package:yaml/yaml.dart';

import '../src/common.dart';
import '../src/fake_process_manager.dart';

const String defaultTemplateArbFileName = 'app_en.arb';
const String defaultOutputFileString = 'output-localization-file.dart';
const String defaultClassNameString = 'AppLocalizations';
const String singleMessageArbFileString = '''
{
  "title": "Title",
  "@title": {
    "description": "Title for the application."
  }
}''';
const String twoMessageArbFileString = '''
{
  "title": "Title",
  "@title": {
    "description": "Title for the application."
  },
  "subtitle": "Subtitle",
  "@subtitle": {
    "description": "Subtitle for the application."
  }
}''';
const String esArbFileName = 'app_es.arb';
const String singleEsMessageArbFileString = '''
{
  "title": "Título"
}''';
const String singleZhMessageArbFileString = '''
{
  "title": "标题"
}''';
const String intlImportDartCode = '''
import 'package:intl/intl.dart' as intl;
''';
const String foundationImportDartCode = '''
import 'package:flutter/foundation.dart';
''';

void _standardFlutterDirectoryL10nSetup(FileSystem fs) {
  final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
    ..createSync(recursive: true);
  l10nDirectory.childFile(defaultTemplateArbFileName)
    .writeAsStringSync(singleMessageArbFileString);
  l10nDirectory.childFile(esArbFileName)
    .writeAsStringSync(singleEsMessageArbFileString);
  fs.file('pubspec.yaml')
    ..createSync(recursive: true)
    ..writeAsStringSync('''
flutter:
  generate: true
''');

}

void main() {
  late MemoryFileSystem fs;
  late BufferLogger logger;
  late Artifacts artifacts;
  late ProcessManager processManager;
  late String defaultL10nPathString;
  late String syntheticPackagePath;
  late String syntheticL10nPackagePath;

  LocalizationsGenerator setupLocalizations(
    Map<String, String> localeToArbFile,
    {
      String? yamlFile,
      String? outputPathString,
      String? outputFileString,
      String? headerString,
      String? headerFile,
      String? untranslatedMessagesFile,
      bool useSyntheticPackage = true,
      bool isFromYaml = false,
      bool usesNullableGetter = true,
      String? inputsAndOutputsListPath,
      List<String>? preferredSupportedLocales,
      bool useDeferredLoading = false,
      bool useEscaping = false,
      bool areResourceAttributeRequired = false,
      bool suppressWarnings = false,
      bool relaxSyntax = false,
      void Function(Directory)? setup,
    }
  ) {
    final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
      ..createSync(recursive: true);
    for (final String locale in localeToArbFile.keys) {
      l10nDirectory.childFile('app_$locale.arb')
        .writeAsStringSync(localeToArbFile[locale]!);
    }
    if (setup != null) {
      setup(l10nDirectory);
    }
    return LocalizationsGenerator(
      fileSystem: fs,
      inputPathString: l10nDirectory.path,
      outputPathString: outputPathString ?? l10nDirectory.path,
      templateArbFileName: defaultTemplateArbFileName,
      outputFileString: outputFileString ?? defaultOutputFileString,
      classNameString: defaultClassNameString,
      headerString: headerString,
      headerFile: headerFile,
      logger: logger,
      untranslatedMessagesFile: untranslatedMessagesFile,
      useSyntheticPackage: useSyntheticPackage,
      inputsAndOutputsListPath: inputsAndOutputsListPath,
      usesNullableGetter: usesNullableGetter,
      preferredSupportedLocales: preferredSupportedLocales,
      useDeferredLoading: useDeferredLoading,
      useEscaping: useEscaping,
      areResourceAttributesRequired: areResourceAttributeRequired,
      suppressWarnings: suppressWarnings,
      useRelaxedSyntax: relaxSyntax,
    )
      ..loadResources()
      ..writeOutputFiles(isFromYaml: isFromYaml);
  }

  String getGeneratedFileContent({String? locale}) {
    final String fileName = locale == null ? 'output-localization-file.dart' : 'output-localization-file_$locale.dart';
    return fs.file(
      fs.path.join(syntheticL10nPackagePath, fileName)
    ).readAsStringSync();
  }

  setUp(() {
    fs = MemoryFileSystem.test();
    logger = BufferLogger.test();
    artifacts = Artifacts.test();
    processManager = FakeProcessManager.empty();

    defaultL10nPathString = fs.path.join('lib', 'l10n');
    syntheticPackagePath = fs.path.join('.dart_tool', 'flutter_gen');
    syntheticL10nPackagePath = fs.path.join(syntheticPackagePath, 'gen_l10n');
    precacheLanguageAndRegionTags();
  });

  group('Setters', () {
    testWithoutContext('setInputDirectory fails if the directory does not exist', () {
      expect(
        () => LocalizationsGenerator.inputDirectoryFromPath(fs, 'lib', fs.directory('bogus')),
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains('Make sure that the correct path was provided'),
        )),
      );
    });

    testWithoutContext('setting className fails if input string is empty', () {
      _standardFlutterDirectoryL10nSetup(fs);
      expect(
        () => LocalizationsGenerator.classNameFromString(''),
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains('cannot be empty'),
        )),
      );
    });

    testWithoutContext('sets absolute path of the target Flutter project', () {
      // Set up project directory.
      final Directory l10nDirectory = fs.currentDirectory
        .childDirectory('absolute')
        .childDirectory('path')
        .childDirectory('to')
        .childDirectory('flutter_project')
        .childDirectory('lib')
        .childDirectory('l10n')
        ..createSync(recursive: true);
      l10nDirectory.childFile(defaultTemplateArbFileName)
        .writeAsStringSync(singleMessageArbFileString);
      l10nDirectory.childFile(esArbFileName)
        .writeAsStringSync(singleEsMessageArbFileString);

      // Run localizations generator in specified absolute path.
      final String flutterProjectPath = fs.path.join('absolute', 'path', 'to', 'flutter_project');
      LocalizationsGenerator(
        fileSystem: fs,
        projectPathString: flutterProjectPath,
        inputPathString: defaultL10nPathString,
        outputPathString: defaultL10nPathString,
        templateArbFileName: defaultTemplateArbFileName,
        outputFileString: defaultOutputFileString,
        classNameString: defaultClassNameString,
        logger: logger,
      )
        ..loadResources()
        ..writeOutputFiles();

      // Output files should be generated in the provided absolute path.
      expect(
        fs.isFileSync(fs.path.join(
          flutterProjectPath,
          '.dart_tool',
          'flutter_gen',
          'gen_l10n',
          'output-localization-file_en.dart',
        )),
        true,
      );
      expect(
        fs.isFileSync(fs.path.join(
          flutterProjectPath,
          '.dart_tool',
          'flutter_gen',
          'gen_l10n',
          'output-localization-file_es.dart',
        )),
        true,
      );
    });

    testWithoutContext('throws error when directory at absolute path does not exist', () {
      // Set up project directory.
      final Directory l10nDirectory = fs.currentDirectory
        .childDirectory('lib')
        .childDirectory('l10n')
        ..createSync(recursive: true);
      l10nDirectory.childFile(defaultTemplateArbFileName)
        .writeAsStringSync(singleMessageArbFileString);
      l10nDirectory.childFile(esArbFileName)
        .writeAsStringSync(singleEsMessageArbFileString);

      // Project path should be intentionally a directory that does not exist.
      expect(
        () => LocalizationsGenerator(
          fileSystem: fs,
          projectPathString: 'absolute/path/to/flutter_project',
          inputPathString: defaultL10nPathString,
          outputPathString: defaultL10nPathString,
          templateArbFileName: defaultTemplateArbFileName,
          outputFileString: defaultOutputFileString,
          classNameString: defaultClassNameString,
          logger: logger,
        ),
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains('Directory does not exist'),
        )),
      );
    });

    testWithoutContext('throws error when arb file does not exist', () {
      // Set up project directory.
      fs.currentDirectory
        .childDirectory('lib')
        .childDirectory('l10n')
        .createSync(recursive: true);

      // Arb file should be nonexistent in the l10n directory.
      expect(
        () => LocalizationsGenerator(
          fileSystem: fs,
          projectPathString: './',
          inputPathString: defaultL10nPathString,
          outputPathString: defaultL10nPathString,
          templateArbFileName: defaultTemplateArbFileName,
          outputFileString: defaultOutputFileString,
          classNameString: defaultClassNameString,
          logger: logger,
        ),
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains(', does not exist.'),
        )),
      );
    });

    group('className should only take valid Dart class names', () {
      setUp(() {
        _standardFlutterDirectoryL10nSetup(fs);
      });

      testWithoutContext('fails on string with spaces', () {
        expect(
          () => LocalizationsGenerator.classNameFromString('String with spaces'),
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            contains('is not a valid public Dart class name'),
          )),
        );
      });

      testWithoutContext('fails on non-alphanumeric symbols', () {
        expect(
          () => LocalizationsGenerator.classNameFromString('TestClass@123'),
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            contains('is not a valid public Dart class name'),
          )),
        );
      });

      testWithoutContext('fails on camel-case', () {
        expect(
          () => LocalizationsGenerator.classNameFromString('camelCaseClassName'),
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            contains('is not a valid public Dart class name'),
          )),
        );
      });

      testWithoutContext('fails when starting with a number', () {
        expect(
          () => LocalizationsGenerator.classNameFromString('123ClassName'),
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            contains('is not a valid public Dart class name'),
          )),
        );
      });
    });
  });

  testWithoutContext('correctly adds a headerString when it is set', () {
    final LocalizationsGenerator generator = setupLocalizations(<String, String>{
      'en': singleMessageArbFileString,
      'es': singleEsMessageArbFileString,
    }, headerString: '/// Sample header');
    expect(generator.header, '/// Sample header');
  });

  testWithoutContext('correctly adds a headerFile when it is set', () {
    final LocalizationsGenerator generator = setupLocalizations(<String, String>{
      'en': singleMessageArbFileString,
      'es': singleEsMessageArbFileString,
    }, headerFile: 'header.txt', setup: (Directory l10nDirectory) {
      l10nDirectory.childFile('header.txt').writeAsStringSync('/// Sample header in a text file');
    });
    expect(generator.header, '/// Sample header in a text file');
  });

  testWithoutContext('sets templateArbFileName with more than one underscore correctly', () {
    setupLocalizations(<String, String>{
      'en': singleMessageArbFileString,
      'es': singleEsMessageArbFileString,
    });
    final Directory outputDirectory = fs.directory(syntheticL10nPackagePath);
    expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
    expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
    expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
  });

  testWithoutContext('filenames with invalid locales should not be recognized', () {
    expect(
      () {
        // This attempts to create 'app_localizations_en_CA_foo.arb'.
        setupLocalizations(<String, String>{
          'en': singleMessageArbFileString,
          'en_CA_foo': singleMessageArbFileString,
        });
      },
      throwsA(isA<L10nException>().having(
        (L10nException e) => e.message,
        'message',
        contains("The following .arb file's locale could not be determined"),
      )),
    );
  });

  testWithoutContext('correctly creates an untranslated messages file (useSyntheticPackage = true)', () {
    final String untranslatedMessagesFilePath = fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json');
    setupLocalizations(<String, String>{
      'en': twoMessageArbFileString,
      'es': singleEsMessageArbFileString,
    }, untranslatedMessagesFile: untranslatedMessagesFilePath);
    final String unimplementedOutputString = fs.file(untranslatedMessagesFilePath).readAsStringSync();
    try {
      // Since ARB file is essentially JSON, decoding it should not fail.
      json.decode(unimplementedOutputString);
    } on Exception {
      fail('Parsing arb file should not fail');
    }
    expect(unimplementedOutputString, contains('es'));
    expect(unimplementedOutputString, contains('subtitle'));
  });

  testWithoutContext('correctly creates an untranslated messages file (useSyntheticPackage = false)', () {
    final String untranslatedMessagesFilePath = fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json');
    setupLocalizations(<String, String>{
      'en': twoMessageArbFileString,
      'es': singleMessageArbFileString,
    }, useSyntheticPackage: false, untranslatedMessagesFile: untranslatedMessagesFilePath);
    final String unimplementedOutputString = fs.file(untranslatedMessagesFilePath).readAsStringSync();
    try {
      // Since ARB file is essentially JSON, decoding it should not fail.
      json.decode(unimplementedOutputString);
    } on Exception {
      fail('Parsing arb file should not fail');
    }
    expect(unimplementedOutputString, contains('es'));
    expect(unimplementedOutputString, contains('subtitle'));
  });

  testWithoutContext(
    'untranslated messages suggestion is printed when translation is missing: '
    'command line message',
    () {
      setupLocalizations(<String, String>{
        'en': twoMessageArbFileString,
        'es': singleEsMessageArbFileString,
      });
      expect(
        logger.statusText,
        contains('To see a detailed report, use the --untranslated-messages-file'),
      );
      expect(
        logger.statusText,
        contains('flutter gen-l10n --untranslated-messages-file=desiredFileName.txt'),
      );
    },
  );

  testWithoutContext(
    'untranslated messages suggestion is printed when translation is missing: '
    'l10n.yaml message',
    () {
      setupLocalizations(<String, String>{
        'en': twoMessageArbFileString,
        'es': singleEsMessageArbFileString,
      }, isFromYaml: true);
      expect(
        logger.statusText,
        contains('To see a detailed report, use the untranslated-messages-file'),
      );
      expect(
        logger.statusText,
        contains('untranslated-messages-file: desiredFileName.txt'),
      );
    },
  );

  testWithoutContext(
    'unimplemented messages suggestion is not printed when all messages '
    'are fully translated',
    () {
      setupLocalizations(<String, String>{
        'en': twoMessageArbFileString,
        'es': twoMessageArbFileString,
      });
      expect(logger.statusText, equals(''));
    },
  );

  testWithoutContext('untranslated messages file included in generated JSON list of outputs', () {
    final String untranslatedMessagesFilePath = fs.path.join('lib', 'l10n', 'unimplemented_message_translations.json');
    setupLocalizations(
      <String, String>{
        'en': twoMessageArbFileString,
        'es': singleEsMessageArbFileString,
      },
      untranslatedMessagesFile: untranslatedMessagesFilePath,
      inputsAndOutputsListPath: syntheticL10nPackagePath,
    );
    final File inputsAndOutputsList = fs.file(
      fs.path.join(syntheticL10nPackagePath, 'gen_l10n_inputs_and_outputs.json')
    );
    expect(inputsAndOutputsList.existsSync(), isTrue);
    final Map<String, dynamic> jsonResult = json.decode(
      inputsAndOutputsList.readAsStringSync(),
    ) as Map<String, dynamic>;
    expect(jsonResult.containsKey('outputs'), isTrue);
    final List<dynamic> outputList = jsonResult['outputs'] as List<dynamic>;
    expect(outputList, contains(contains('unimplemented_message_translations.json')));
  });

  testWithoutContext(
    'uses inputPathString as outputPathString when the outputPathString is '
    'null while not using the synthetic package option',
    () {
      _standardFlutterDirectoryL10nSetup(fs);
      LocalizationsGenerator(
        fileSystem: fs,
        inputPathString: defaultL10nPathString,
        // outputPathString is intentionally not defined
        templateArbFileName: defaultTemplateArbFileName,
        outputFileString: defaultOutputFileString,
        classNameString: defaultClassNameString,
        useSyntheticPackage: false,
        logger: logger,
      )
        ..loadResources()
        ..writeOutputFiles();

      final Directory outputDirectory = fs.directory('lib').childDirectory('l10n');
      expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
      expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
      expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
    },
  );

  testWithoutContext(
    'correctly generates output files in non-default output directory if it '
    'already exists while not using the synthetic package option',
    () {
      final Directory l10nDirectory = fs.currentDirectory
        .childDirectory('lib')
        .childDirectory('l10n')
        ..createSync(recursive: true);
      // Create the directory 'lib/l10n/output'.
      l10nDirectory.childDirectory('output');

      l10nDirectory
        .childFile(defaultTemplateArbFileName)
        .writeAsStringSync(singleMessageArbFileString);
      l10nDirectory
        .childFile(esArbFileName)
        .writeAsStringSync(singleEsMessageArbFileString);

      LocalizationsGenerator(
        fileSystem: fs,
        inputPathString: defaultL10nPathString,
        outputPathString: fs.path.join('lib', 'l10n', 'output'),
        templateArbFileName: defaultTemplateArbFileName,
        outputFileString: defaultOutputFileString,
        classNameString: defaultClassNameString,
        useSyntheticPackage: false,
        logger: logger,
      )
        ..loadResources()
        ..writeOutputFiles();

      final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output');
      expect(outputDirectory.existsSync(), isTrue);
      expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
      expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
      expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
    },
  );

  testWithoutContext(
    'correctly creates output directory if it does not exist and writes files '
    'in it while not using the synthetic package option',
    () {
      _standardFlutterDirectoryL10nSetup(fs);

      LocalizationsGenerator(
        fileSystem: fs,
          inputPathString: defaultL10nPathString,
          outputPathString: fs.path.join('lib', 'l10n', 'output'),
          templateArbFileName: defaultTemplateArbFileName,
          outputFileString: defaultOutputFileString,
          classNameString: defaultClassNameString,
          useSyntheticPackage: false,
          logger: logger,
        )
        ..loadResources()
        ..writeOutputFiles();

      final Directory outputDirectory = fs.directory('lib').childDirectory('l10n').childDirectory('output');
      expect(outputDirectory.existsSync(), isTrue);
      expect(outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
      expect(outputDirectory.childFile('output-localization-file_en.dart').existsSync(), isTrue);
      expect(outputDirectory.childFile('output-localization-file_es.dart').existsSync(), isTrue);
    },
  );

  testWithoutContext(
    'generates nullable localizations class getter via static `of` method '
    'by default',
    () {
      final LocalizationsGenerator generator = setupLocalizations(<String, String>{
        'en': singleMessageArbFileString,
        'es': singleEsMessageArbFileString,
      });
      expect(generator.outputDirectory.existsSync(), isTrue);
      expect(generator.outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
      expect(
        generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
        contains('static AppLocalizations? of(BuildContext context)'),
      );
      expect(
        generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
        contains('return Localizations.of<AppLocalizations>(context, AppLocalizations);'),
      );
    },
  );

  testWithoutContext(
    'can generate non-nullable localizations class getter via static `of` method ',
    () {
      final LocalizationsGenerator generator = setupLocalizations(<String, String>{
        'en': singleMessageArbFileString,
        'es': singleEsMessageArbFileString,
      }, usesNullableGetter: false);
      expect(generator.outputDirectory.existsSync(), isTrue);
      expect(generator.outputDirectory.childFile('output-localization-file.dart').existsSync(), isTrue);
      expect(
        generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
        contains('static AppLocalizations of(BuildContext context)'),
      );
      expect(
        generator.outputDirectory.childFile('output-localization-file.dart').readAsStringSync(),
        contains('return Localizations.of<AppLocalizations>(context, AppLocalizations)!;'),
      );
    },
  );

  testWithoutContext('creates list of inputs and outputs when file path is specified', () {
    setupLocalizations(<String, String>{
      'en': singleMessageArbFileString,
      'es': singleEsMessageArbFileString,
    }, inputsAndOutputsListPath: syntheticL10nPackagePath);
    final File inputsAndOutputsList = fs.file(
      fs.path.join(syntheticL10nPackagePath, 'gen_l10n_inputs_and_outputs.json'),
    );
    expect(inputsAndOutputsList.existsSync(), isTrue);

    final Map<String, dynamic> jsonResult = json.decode(inputsAndOutputsList.readAsStringSync()) as Map<String, dynamic>;
    expect(jsonResult.containsKey('inputs'), isTrue);
    final List<dynamic> inputList = jsonResult['inputs'] as List<dynamic>;
    expect(inputList, contains(fs.path.absolute('lib', 'l10n', 'app_en.arb')));
    expect(inputList, contains(fs.path.absolute('lib', 'l10n', 'app_es.arb')));

    expect(jsonResult.containsKey('outputs'), isTrue);
    final List<dynamic> outputList = jsonResult['outputs'] as List<dynamic>;
    expect(outputList, contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file.dart')));
    expect(outputList, contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file_en.dart')));
    expect(outputList, contains(fs.path.absolute(syntheticL10nPackagePath, 'output-localization-file_es.dart')));
  });

  testWithoutContext('setting both a headerString and a headerFile should fail', () {
    expect(
      () {
        setupLocalizations(
          <String, String>{
            'en': singleMessageArbFileString,
            'es': singleEsMessageArbFileString,
          },
          headerString: '/// Sample header in a text file',
          headerFile: 'header.txt',
          setup: (Directory l10nDirectory) {
            l10nDirectory.childFile('header.txt').writeAsStringSync('/// Sample header in a text file');
          },
        );
      },
      throwsA(isA<L10nException>().having(
        (L10nException e) => e.message,
        'message',
        contains('Cannot accept both header and header file arguments'),
      )),
    );
  });

  testWithoutContext('setting a headerFile that does not exist should fail', () {
    expect(
      () {
        setupLocalizations(<String, String>{
          'en': singleMessageArbFileString,
          'es': singleEsMessageArbFileString,
        }, headerFile: 'header.txt');
      },
      throwsA(isA<L10nException>().having(
        (L10nException e) => e.message,
        'message',
        contains('Failed to read header file'),
      )),
    );
  });

  group('generateLocalizations', () {
    // Regression test for https://github.com/flutter/flutter/issues/119593
    testWithoutContext('other logs from flutter_tools does not affect gen-l10n', () async {
      _standardFlutterDirectoryL10nSetup(fs);

      final Logger logger = BufferLogger.test();
      logger.printError('An error output from a different tool in flutter_tools');

      // Should run without error.
      await generateLocalizations(
        fileSystem: fs,
        options: LocalizationOptions(
          arbDir: Uri.directory(defaultL10nPathString).path,
          outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
          templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
          syntheticPackage: false,
        ),
        logger: logger,
        projectDir: fs.currentDirectory,
        dependenciesDir: fs.currentDirectory,
        artifacts: artifacts,
        processManager: processManager,
      );
    });

    testWithoutContext('forwards arguments correctly', () async {
      _standardFlutterDirectoryL10nSetup(fs);
      final LocalizationOptions options = LocalizationOptions(
        header: 'HEADER',
        arbDir: Uri.directory(defaultL10nPathString).path,
        useDeferredLoading: true,
        outputClass: 'Foo',
        outputLocalizationFile: Uri.file('bar.dart', windows: false).path,
        outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
        preferredSupportedLocales: <String>['es'],
        templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
        untranslatedMessagesFile: Uri.file('untranslated', windows: false).path,
        syntheticPackage: false,
        requiredResourceAttributes: true,
        nullableGetter: false,
      );

      // Verify that values are correctly passed through the localizations target.
      final LocalizationsGenerator generator = await generateLocalizations(
        fileSystem: fs,
        options: options,
        logger: logger,
        projectDir: fs.currentDirectory,
        dependenciesDir: fs.currentDirectory,
        artifacts: artifacts,
        processManager: processManager,
      );

      expect(generator.inputDirectory.path, '/lib/l10n/');
      expect(generator.outputDirectory.path, '/lib/l10n/');
      expect(generator.templateArbFile.path, '/lib/l10n/app_en.arb');
      expect(generator.baseOutputFile.path, '/lib/l10n/bar.dart');
      expect(generator.className, 'Foo');
      expect(generator.preferredSupportedLocales.single, LocaleInfo.fromString('es'));
      expect(generator.header, 'HEADER');
      expect(generator.useDeferredLoading, isTrue);
      expect(generator.inputsAndOutputsListFile?.path, '/gen_l10n_inputs_and_outputs.json');
      expect(generator.useSyntheticPackage, isFalse);
      expect(generator.projectDirectory?.path, '/');
      expect(generator.areResourceAttributesRequired, isTrue);
      expect(generator.untranslatedMessagesFile?.path, 'untranslated');
      expect(generator.usesNullableGetter, isFalse);

      // Just validate one file.
      expect(fs.file('/lib/l10n/bar_en.dart').readAsStringSync(), '''
HEADER

import 'bar.dart';

/// The translations for English (`en`).
class FooEn extends Foo {
  FooEn([String locale = 'en']) : super(locale);

  @override
  String get title => 'Title';
}
''');
    });

    testWithoutContext('throws exception on missing flutter: generate: true flag', () async {
      _standardFlutterDirectoryL10nSetup(fs);

      // Missing flutter: generate: true should throw exception.
      fs.file('pubspec.yaml')
        ..createSync(recursive: true)
        ..writeAsStringSync('''
flutter:
  uses-material-design: true
''');

      final LocalizationOptions options = LocalizationOptions(
        header: 'HEADER',
        headerFile: Uri.file('header', windows: false).path,
        arbDir: Uri.file('arb', windows: false).path,
        useDeferredLoading: true,
        outputClass: 'Foo',
        outputLocalizationFile: Uri.file('bar', windows: false).path,
        preferredSupportedLocales: <String>['en_US'],
        templateArbFile: Uri.file('example.arb', windows: false).path,
        untranslatedMessagesFile: Uri.file('untranslated', windows: false).path,
      );

      expect(
        () => generateLocalizations(
          fileSystem: fs,
          options: options,
          logger: BufferLogger.test(),
          projectDir: fs.currentDirectory,
          dependenciesDir: fs.currentDirectory,
          artifacts: artifacts,
          processManager: processManager,
        ),
        throwsToolExit(
          message: 'Attempted to generate localizations code without having the '
              'flutter: generate flag turned on.',
        ),
      );
    });

    testWithoutContext('uses the same line terminator as pubspec.yaml', () async {
      _standardFlutterDirectoryL10nSetup(fs);

      fs.file('pubspec.yaml')
        ..createSync(recursive: true)
        ..writeAsStringSync('''
flutter:\r
  generate: true\r
''');

      final LocalizationOptions options = LocalizationOptions(
        arbDir: fs.path.join('lib', 'l10n'),
        outputClass: defaultClassNameString,
        outputLocalizationFile: defaultOutputFileString,
      );
      await generateLocalizations(
        fileSystem: fs,
        options: options,
        logger: BufferLogger.test(),
        projectDir: fs.currentDirectory,
        dependenciesDir: fs.currentDirectory,
        artifacts: artifacts,
        processManager: processManager,
      );
      final String content = getGeneratedFileContent(locale: 'en');
      expect(content, contains('\r\n'));
    });

    testWithoutContext('blank lines generated nicely', () async {
      _standardFlutterDirectoryL10nSetup(fs);

      // Test without headers.
      await generateLocalizations(
        fileSystem: fs,
        options: LocalizationOptions(
          arbDir: Uri.directory(defaultL10nPathString).path,
          outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
          templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
          syntheticPackage: false,
        ),
        logger: BufferLogger.test(),
        projectDir: fs.currentDirectory,
        dependenciesDir: fs.currentDirectory,
        artifacts: artifacts,
        processManager: processManager,
      );

      expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), '''
import 'app_localizations.dart';

/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
  AppLocalizationsEn([String locale = 'en']) : super(locale);

  @override
  String get title => 'Title';
}
''');

    // Test with headers.
    await generateLocalizations(
      fileSystem: fs,
      options: LocalizationOptions(
        header: 'HEADER',
        arbDir: Uri.directory(defaultL10nPathString).path,
        outputDir: Uri.directory(defaultL10nPathString, windows: false).path,
        templateArbFile: Uri.file(defaultTemplateArbFileName, windows: false).path,
        syntheticPackage: false,
      ),
      logger: logger,
      projectDir: fs.currentDirectory,
      dependenciesDir: fs.currentDirectory,
      artifacts: artifacts,
      processManager: processManager,
    );

    expect(fs.file('/lib/l10n/app_localizations_en.dart').readAsStringSync(), '''
HEADER

import 'app_localizations.dart';

/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
  AppLocalizationsEn([String locale = 'en']) : super(locale);

  @override
  String get title => 'Title';
}
''');
    });
  });

  group('loadResources', () {
    testWithoutContext('correctly initializes supportedLocales and supportedLanguageCodes properties', () {
      _standardFlutterDirectoryL10nSetup(fs);

      final LocalizationsGenerator generator = LocalizationsGenerator(
        fileSystem: fs,
        inputPathString: defaultL10nPathString,
        outputPathString: defaultL10nPathString,
        templateArbFileName: defaultTemplateArbFileName,
        outputFileString: defaultOutputFileString,
        classNameString: defaultClassNameString,
        logger: logger,
      )
        ..loadResources();

      expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true);
      expect(generator.supportedLocales.contains(LocaleInfo.fromString('es')), true);
    });

    testWithoutContext('correctly sorts supportedLocales and supportedLanguageCodes alphabetically', () {
      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
        ..createSync(recursive: true);
      // Write files in non-alphabetical order so that read performs in that order
      l10nDirectory.childFile('app_zh.arb')
        .writeAsStringSync(singleZhMessageArbFileString);
      l10nDirectory.childFile('app_es.arb')
        .writeAsStringSync(singleEsMessageArbFileString);
      l10nDirectory.childFile('app_en.arb')
        .writeAsStringSync(singleMessageArbFileString);

      final LocalizationsGenerator generator = LocalizationsGenerator(
        fileSystem: fs,
        inputPathString: defaultL10nPathString,
        outputPathString: defaultL10nPathString,
        templateArbFileName: defaultTemplateArbFileName,
        outputFileString: defaultOutputFileString,
        classNameString: defaultClassNameString,
        logger: logger,
      )
        ..loadResources();

      expect(generator.supportedLocales.first, LocaleInfo.fromString('en'));
      expect(generator.supportedLocales.elementAt(1), LocaleInfo.fromString('es'));
      expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('zh'));
    });

    testWithoutContext('adds preferred locales to the top of supportedLocales and supportedLanguageCodes', () {
      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
        ..createSync(recursive: true);
      l10nDirectory.childFile('app_en.arb')
        .writeAsStringSync(singleMessageArbFileString);
      l10nDirectory.childFile('app_es.arb')
        .writeAsStringSync(singleEsMessageArbFileString);
      l10nDirectory.childFile('app_zh.arb')
        .writeAsStringSync(singleZhMessageArbFileString);

      const List<String> preferredSupportedLocale = <String>['zh', 'es'];
      final LocalizationsGenerator generator = LocalizationsGenerator(
        fileSystem: fs,
        inputPathString: defaultL10nPathString,
        outputPathString: defaultL10nPathString,
        templateArbFileName: defaultTemplateArbFileName,
        outputFileString: defaultOutputFileString,
        classNameString: defaultClassNameString,
        preferredSupportedLocales: preferredSupportedLocale,
        logger: logger,
      )
        ..loadResources();

      expect(generator.supportedLocales.first, LocaleInfo.fromString('zh'));
      expect(generator.supportedLocales.elementAt(1), LocaleInfo.fromString('es'));
      expect(generator.supportedLocales.elementAt(2), LocaleInfo.fromString('en'));
    });

    testWithoutContext(
      'throws an error attempting to add preferred locales when there is no corresponding arb file for that locale',
      () {
        final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
          ..createSync(recursive: true);
        l10nDirectory.childFile('app_en.arb')
          .writeAsStringSync(singleMessageArbFileString);
        l10nDirectory.childFile('app_es.arb')
          .writeAsStringSync(singleEsMessageArbFileString);
        l10nDirectory.childFile('app_zh.arb')
          .writeAsStringSync(singleZhMessageArbFileString);

        const List<String> preferredSupportedLocale = <String>['am', 'es'];
        expect(
          () {
            LocalizationsGenerator(
              fileSystem: fs,
              inputPathString: defaultL10nPathString,
              outputPathString: defaultL10nPathString,
              templateArbFileName: defaultTemplateArbFileName,
              outputFileString: defaultOutputFileString,
              classNameString: defaultClassNameString,
              preferredSupportedLocales: preferredSupportedLocale,
              logger: logger,
            ).loadResources();
          },
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            contains("The preferred supported locale, 'am', cannot be added."),
          )),
        );
      },
    );

    testWithoutContext('correctly sorts arbPathString alphabetically', () {
      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
        ..createSync(recursive: true);
      // Write files in non-alphabetical order so that read performs in that order
      l10nDirectory.childFile('app_zh.arb')
        .writeAsStringSync(singleZhMessageArbFileString);
      l10nDirectory.childFile('app_es.arb')
        .writeAsStringSync(singleEsMessageArbFileString);
      l10nDirectory.childFile('app_en.arb')
        .writeAsStringSync(singleMessageArbFileString);

      final LocalizationsGenerator generator = LocalizationsGenerator(
        fileSystem: fs,
        inputPathString: defaultL10nPathString,
        outputPathString: defaultL10nPathString,
        templateArbFileName: defaultTemplateArbFileName,
        outputFileString: defaultOutputFileString,
        classNameString: defaultClassNameString,
        logger: logger,
      )
        ..loadResources();

      expect(generator.arbPathStrings.first, fs.path.join('lib', 'l10n', 'app_en.arb'));
      expect(generator.arbPathStrings.elementAt(1), fs.path.join('lib', 'l10n', 'app_es.arb'));
      expect(generator.arbPathStrings.elementAt(2), fs.path.join('lib', 'l10n', 'app_zh.arb'));
    });

    testWithoutContext('correctly parses @@locale property in arb file', () {
      const String arbFileWithEnLocale = '''
{
  "@@locale": "en",
  "title": "Title",
  "@title": {
    "description": "Title for the application"
  }
}''';

      const String arbFileWithZhLocale = '''
{
  "@@locale": "zh",
  "title": "标题",
  "@title": {
    "description": "Title for the application"
  }
}''';

      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
        ..createSync(recursive: true);
      l10nDirectory.childFile('first_file.arb')
        .writeAsStringSync(arbFileWithEnLocale);
      l10nDirectory.childFile('second_file.arb')
        .writeAsStringSync(arbFileWithZhLocale);

      final LocalizationsGenerator generator = LocalizationsGenerator(
        fileSystem: fs,
        inputPathString: defaultL10nPathString,
        outputPathString: defaultL10nPathString,
        templateArbFileName: 'first_file.arb',
        outputFileString: defaultOutputFileString,
        classNameString: defaultClassNameString,
        logger: logger,
      )
        ..loadResources();

      expect(generator.supportedLocales.contains(LocaleInfo.fromString('en')), true);
      expect(generator.supportedLocales.contains(LocaleInfo.fromString('zh')), true);
    });

    testWithoutContext('correctly requires @@locale property in arb file to match the filename locale suffix', () {
      const String arbFileWithEnLocale = '''
{
  "@@locale": "en",
  "title": "Stocks",
  "@title": {
    "description": "Title for the Stocks application"
  }
}''';

      const String arbFileWithZhLocale = '''
{
  "@@locale": "zh",
  "title": "标题",
  "@title": {
    "description": "Title for the Stocks application"
  }
}''';

      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
        ..createSync(recursive: true);
      l10nDirectory.childFile('app_es.arb')
        .writeAsStringSync(arbFileWithEnLocale);
      l10nDirectory.childFile('app_am.arb')
        .writeAsStringSync(arbFileWithZhLocale);

      expect(
        () {
          LocalizationsGenerator(
            fileSystem: fs,
            inputPathString: defaultL10nPathString,
            outputPathString: defaultL10nPathString,
            templateArbFileName: 'app_es.arb',
            outputFileString: defaultOutputFileString,
            classNameString: defaultClassNameString,
            logger: logger,
          ).loadResources();
        },
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains('The locale specified in @@locale and the arb filename do not match.'),
        )),
      );
    });

    testWithoutContext("throws when arb file's locale could not be determined", () {
      fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
        ..createSync(recursive: true)
        ..childFile('app.arb')
        .writeAsStringSync(singleMessageArbFileString);
      expect(
        () {
          LocalizationsGenerator(
            fileSystem: fs,
            inputPathString: defaultL10nPathString,
            outputPathString: defaultL10nPathString,
            templateArbFileName: 'app.arb',
            outputFileString: defaultOutputFileString,
            classNameString: defaultClassNameString,
            logger: logger,
          ).loadResources();
        },
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains('locale could not be determined'),
        )),
      );
    });

    testWithoutContext('throws when an empty string is used as a key', () {
      const String arbFileStringWithEmptyResourceId = '''
{
  "market": "MARKET",
  "": {
    "description": "This key is invalid"
  }
}''';

      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
        ..createSync(recursive: true);
      l10nDirectory.childFile('app_en.arb')
        .writeAsStringSync(arbFileStringWithEmptyResourceId);

      expect(
        () => LocalizationsGenerator(
          fileSystem: fs,
          inputPathString: defaultL10nPathString,
          outputPathString: defaultL10nPathString,
          templateArbFileName: 'app_en.arb',
          outputFileString: defaultOutputFileString,
          classNameString: defaultClassNameString,
          logger: logger,
        ).loadResources(),
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains('Invalid ARB resource name ""'),
        )),
      );
    });

    testWithoutContext('throws when the same locale is detected more than once', () {
      const String secondMessageArbFileString = '''
{
  "market": "MARKET",
  "@market": {
    "description": "Label for the Market tab"
  }
}''';

      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
        ..createSync(recursive: true);
      l10nDirectory.childFile('app_en.arb')
        .writeAsStringSync(singleMessageArbFileString);
      l10nDirectory.childFile('app2_en.arb')
        .writeAsStringSync(secondMessageArbFileString);

      expect(
        () {
          LocalizationsGenerator(
            fileSystem: fs,
            inputPathString: defaultL10nPathString,
            outputPathString: defaultL10nPathString,
            templateArbFileName: 'app_en.arb',
            outputFileString: defaultOutputFileString,
            classNameString: defaultClassNameString,
            logger: logger,
          ).loadResources();
        },
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains("Multiple arb files with the same 'en' locale detected"),
        )),
      );
    });

    testWithoutContext('throws when the base locale does not exist', () {
      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
        ..createSync(recursive: true);
      l10nDirectory.childFile('app_en_US.arb')
        .writeAsStringSync(singleMessageArbFileString);

      expect(
        () {
          LocalizationsGenerator(
            fileSystem: fs,
            inputPathString: defaultL10nPathString,
            outputPathString: defaultL10nPathString,
            templateArbFileName: 'app_en_US.arb',
            outputFileString: defaultOutputFileString,
            classNameString: defaultClassNameString,
            logger: logger,
          ).loadResources();
        },
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains('Arb file for a fallback, en, does not exist'),
        )),
      );
    });

    testWithoutContext('AppResourceBundle throws if file contains non-string value', () {
      const String inputPathString = 'lib/l10n';
      const String templateArbFileName = 'app_en.arb';
      const String outputFileString = 'app_localizations.dart';
      const String classNameString = 'AppLocalizations';

      fs.file(fs.path.join(inputPathString, templateArbFileName))
        ..createSync(recursive: true)
        ..writeAsStringSync('{ "helloWorld": "Hello World!" }');
      fs.file(fs.path.join(inputPathString, 'app_es.arb'))
        ..createSync(recursive: true)
        ..writeAsStringSync('{ "helloWorld": {} }');

      final LocalizationsGenerator generator = LocalizationsGenerator(
        fileSystem: fs,
        inputPathString: inputPathString,
        templateArbFileName: templateArbFileName,
        outputFileString: outputFileString,
        classNameString: classNameString,
        logger: logger,
      );
      expect(
          () => generator.loadResources(),
          throwsToolExit(message: 'Localized message for key "helloWorld" in '
            '"lib/l10n/app_es.arb" is not a string.'),
      );
    });
  });

  group('writeOutputFiles', () {
    testWithoutContext('multiple messages with syntax error all log their errors', () {
      try {
        setupLocalizations(<String, String>{
          'en': r'''
{
  "msg1": "{",
  "msg2": "{ {"
}'''});
      } on L10nException catch (error) {
        expect(error.message, equals('Found syntax errors.'));
        expect(logger.errorText, contains('''
[app_en.arb:msg1] ICU Syntax Error: Expected "identifier" but found no tokens.
    {
      ^
[app_en.arb:msg2] ICU Syntax Error: Expected "identifier" but found "{".
    { {
      ^'''));
      }
    });

    testWithoutContext('no description generates generic comment', () {
      setupLocalizations(<String, String>{
        'en': r'''
{
  "helloWorld": "Hello world!"
}'''
      });
      expect(getGeneratedFileContent(), contains('/// No description provided for @helloWorld.'));
    });

    testWithoutContext('multiline descriptions are correctly formatted as comments', () {
      setupLocalizations(<String, String>{
        'en': r'''
{
  "helloWorld": "Hello world!",
  "@helloWorld": {
    "description": "The generic example string in every language.\nUse this for tests!"
  }
}'''});
      expect(getGeneratedFileContent(), contains('''
  /// The generic example string in every language.
  /// Use this for tests!'''));
    });

    testWithoutContext('message without placeholders - should generate code comment with description and template message translation', () {
      setupLocalizations(<String, String> {
        'en': singleMessageArbFileString,
        'es': singleEsMessageArbFileString,
      });
      final String content = getGeneratedFileContent();
      expect(content, contains('/// Title for the application.'));
      expect(content, contains('''
  /// In en, this message translates to:
  /// **'Title'**'''));
    });

    testWithoutContext('template message translation handles newline characters', () {
      setupLocalizations(<String, String>{
        'en': r'''
{
  "title": "Title \n of the application",
  "@title": {
    "description": "Title for the application."
  }
}''',
        'es': singleEsMessageArbFileString
      });
      final String content = getGeneratedFileContent();
      expect(content, contains('/// Title for the application.'));
      expect(content, contains(r'''
  /// In en, this message translates to:
  /// **'Title \n of the application'**'''));
    });

    testWithoutContext('message with placeholders - should generate code comment with description and template message translation', () {
      setupLocalizations(<String, String>{
        'en': r'''
{
  "price": "The price of this item is: ${price}",
  "@price": {
    "description": "The price of an online shopping cart item.",
    "placeholders": {
      "price": {
        "type": "double",
        "format": "decimalPattern"
      }
    }
  }
}''',
        'es': r'''
{
  "price": "El precio de este artículo es: ${price}"
}'''
      });
      final String content = getGeneratedFileContent();
      expect(content, contains('/// The price of an online shopping cart item.'));
      expect(content, contains(r'''
  /// In en, this message translates to:
  /// **'The price of this item is: \${price}'**'''));
    });

    testWithoutContext('should generate a file per language', () {
      setupLocalizations(<String, String>{
        'en': singleMessageArbFileString,
        'en_CA': '''
{
  "title": "Canadian Title"
}'''
      });
      expect(getGeneratedFileContent(locale: 'en'), contains('class AppLocalizationsEn extends AppLocalizations'));
      expect(getGeneratedFileContent(locale: 'en'), contains('class AppLocalizationsEnCa extends AppLocalizationsEn'));
      expect(() => getGeneratedFileContent(locale: 'en_US'), throwsException);
    });

    testWithoutContext('language imports are sorted when preferredSupportedLocaleString is given', () {
      const List<String> preferredSupportedLocales = <String>['zh'];
      setupLocalizations(<String, String>{
        'en': singleMessageArbFileString,
        'zh': singleZhMessageArbFileString,
        'es': singleEsMessageArbFileString,
      }, preferredSupportedLocales: preferredSupportedLocales);
      final String content = getGeneratedFileContent();
      expect(content, contains(
'''
import 'output-localization-file_en.dart';
import 'output-localization-file_es.dart';
import 'output-localization-file_zh.dart';
'''));
    });

    // Regression test for https://github.com/flutter/flutter/issues/88356
    testWithoutContext('full output file suffix is retained', () {
      setupLocalizations(<String, String>{
        'en': singleMessageArbFileString,
      }, outputFileString: 'output-localization-file.g.dart');
      final String baseLocalizationsFile = fs.file(
        fs.path.join(syntheticL10nPackagePath, 'output-localization-file.g.dart'),
      ).readAsStringSync();
      expect(baseLocalizationsFile, contains(
'''
import 'output-localization-file_en.g.dart';
'''));

      final String englishLocalizationsFile = fs.file(
        fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.g.dart'),
      ).readAsStringSync();
      expect(englishLocalizationsFile, contains(
'''
import 'output-localization-file.g.dart';
'''));
    });

    testWithoutContext('throws an exception when invalid output file name is passed in', () {
      expect(
        () {
          setupLocalizations(<String, String>{
            'en': singleMessageArbFileString,
          }, outputFileString: 'asdf');
        },
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          allOf(
            contains('output-localization-file'),
            contains('asdf'),
            contains('is invalid'),
            contains('The file name must have a .dart extension.'),
          ),
        )),
      );
      expect(
        () {
          setupLocalizations(<String, String>{
            'en': singleMessageArbFileString,
          }, outputFileString: '.g.dart');
        },
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          allOf(
            contains('output-localization-file'),
            contains('.g.dart'),
            contains('is invalid'),
            contains('The base name cannot be empty.'),
          ),
        )),
      );
    });

    testWithoutContext('imports are deferred and loaded when useDeferredImports are set', () {
      setupLocalizations(<String, String>{
        'en': singleMessageArbFileString,
      }, useDeferredLoading: true);
      final String content = getGeneratedFileContent();
      expect(content, contains(
'''
import 'output-localization-file_en.dart' deferred as output-localization-file_en;
'''));
      expect(content, contains('output-localization-file_en.loadLibrary()'));
    });

    group('placeholder tests', () {
      testWithoutContext('should automatically infer placeholders that are not explicitly defined', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "helloWorld": "Hello {name}"
}'''
        });
        final String content = getGeneratedFileContent(locale: 'en');
        expect(content, contains('String helloWorld(Object name) {'));
      });

      testWithoutContext('placeholder parameter list should be consistent between languages', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "helloWorld": "Hello {name}",
  "@helloWorld": {
    "placeholders": {
      "name": {}
    }
  }
}''',
          'es': '''
{
  "helloWorld": "Hola"
}
''',
        });
        expect(getGeneratedFileContent(locale: 'en'), contains('String helloWorld(Object name) {'));
        expect(getGeneratedFileContent(locale: 'es'), contains('String helloWorld(Object name) {'));
      });

      testWithoutContext('braces are ignored as special characters if placeholder does not exist', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "helloWorld": "Hello {name}",
  "@@helloWorld": {
    "placeholders": {
      "names": {}
    }
  }
}'''
        }, relaxSyntax: true);
        final String content = getGeneratedFileContent(locale: 'en');
        expect(content, contains("String get helloWorld => 'Hello {name}'"));
      });
    });

    group('DateTime tests', () {
      testWithoutContext('imports package:intl', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "@@locale": "en",
  "springBegins": "Spring begins on {springStartDate}",
  "@springBegins": {
    "description": "The first day of spring",
    "placeholders": {
      "springStartDate": {
        "type": "DateTime",
        "format": "yMd"
      }
    }
  }
}'''
        });
        expect(getGeneratedFileContent(locale: 'en'), contains(intlImportDartCode));
      });

      testWithoutContext('throws an exception when improperly formatted date is passed in', () {
        expect(
          () {
            setupLocalizations(<String, String>{
              'en': '''
{
  "springBegins": "Spring begins on {springStartDate}",
  "@springBegins": {
    "placeholders": {
      "springStartDate": {
        "type": "DateTime",
        "format": "asdf"
      }
    }
  }
}'''
            });
          },
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            allOf(
              contains('asdf'),
              contains('springStartDate'),
              contains('does not have a corresponding DateFormat'),
            ),
          )),
        );
      });

      testWithoutContext('use standard date format whenever possible', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "springBegins": "Spring begins on {springStartDate}",
  "@springBegins": {
    "placeholders": {
      "springStartDate": {
        "type": "DateTime",
        "format": "yMd",
        "isCustomDateFormat": "true"
      }
    }
  }
}'''
        });
        final String content = getGeneratedFileContent(locale: 'en');
        expect(content, contains('DateFormat.yMd(localeName)'));
      });

      testWithoutContext('handle arbitrary formatted date', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "@@locale": "en",
  "springBegins": "Spring begins on {springStartDate}",
  "@springBegins": {
    "description": "The first day of spring",
    "placeholders": {
      "springStartDate": {
        "type": "DateTime",
        "format": "asdf o'clock",
        "isCustomDateFormat": "true"
      }
    }
  }
}'''
        });
        final String content = getGeneratedFileContent(locale: 'en');
        expect(content, contains(r"DateFormat('asdf o\'clock', localeName)"));
      });

      testWithoutContext('throws an exception when no format attribute is passed in', () {
        expect(
          () {
            setupLocalizations(<String, String>{
              'en': '''
{
  "springBegins": "Spring begins on {springStartDate}",
  "@springBegins": {
    "description": "The first day of spring",
    "placeholders": {
      "springStartDate": {
        "type": "DateTime"
      }
    }
  }
}'''
            });
          },
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            contains('the "format" attribute needs to be set'),
          )),
        );
      });
    });

    group('NumberFormat tests', () {
      testWithoutContext('imports package:intl', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "courseCompletion": "You have completed {progress} of the course.",
  "@courseCompletion": {
    "description": "The amount of progress the student has made in their class.",
    "placeholders": {
      "progress": {
        "type": "double",
        "format": "percentPattern"
      }
    }
  }
}'''
        });
        final String content = getGeneratedFileContent(locale: 'en');
        expect(content, contains(intlImportDartCode));
      });

      testWithoutContext('throws an exception when improperly formatted number is passed in', () {
       expect(
          () {
            setupLocalizations(<String, String>{
              'en': '''
{
  "courseCompletion": "You have completed {progress} of the course.",
  "@courseCompletion": {
    "description": "The amount of progress the student has made in their class.",
    "placeholders": {
      "progress": {
        "type": "double",
        "format": "asdf"
      }
    }
  }
}'''
            });
          },
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            allOf(
              contains('asdf'),
              contains('progress'),
              contains('does not have a corresponding NumberFormat'),
            ),
          )),
        );
      });
    });

    group('plural messages', () {
      testWithoutContext('intl package import should be omitted in subclass files when no plurals are included', () {
        setupLocalizations(<String, String>{
          'en': singleMessageArbFileString,
          'es': singleEsMessageArbFileString,
        });
        expect(getGeneratedFileContent(locale: 'es'), isNot(contains(intlImportDartCode)));
      });

      testWithoutContext('warnings are generated when plural parts are repeated', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "helloWorlds": "{count,plural, =0{Hello}zero{hello} other{hi}}",
  "@helloWorlds": {
    "description": "Properly formatted but has redundant zero cases."
  }
}'''
        });
        expect(logger.hadWarningOutput, isTrue);
        expect(logger.warningText, contains('''
[app_en.arb:helloWorlds] ICU Syntax Warning: The plural part specified below is overridden by a later plural part.
    {count,plural, =0{Hello}zero{hello} other{hi}}
                   ^'''));
      });

      testWithoutContext('undefined plural cases throws syntax error', () {
        try {
          setupLocalizations(<String, String>{
            'en': '''
{
  "count": "{count,plural, =0{None} =1{One} =2{Two} =3{Undefined Behavior!} other{Hmm...}}"
}'''
          });
        } on L10nException catch (error) {
          expect(error.message, contains('Found syntax errors.'));
          expect(logger.hadErrorOutput, isTrue);
          expect(logger.errorText, contains('''
[app_en.arb:count] The plural cases must be one of "=0", "=1", "=2", "zero", "one", "two", "few", "many", or "other.
    3 is not a valid plural case.
    {count,plural, =0{None} =1{One} =2{Two} =3{Undefined Behavior!} other{Hmm...}}
                                            ^'''));
        }
      });

      testWithoutContext('should automatically infer plural placeholders that are not explicitly defined', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "helloWorlds": "{count,plural, =0{Hello}=1{Hello World}=2{Hello two worlds}few{Hello {count} worlds}many{Hello all {count} worlds}other{Hello other {count} worlds}}",
  "@helloWorlds": {
    "description": "Improperly formatted since it has no placeholder attribute."
  }
}'''
        });
        expect(getGeneratedFileContent(locale: 'en'), contains('String helloWorlds(num count) {'));
      });

      testWithoutContext('should throw attempting to generate a plural message with incorrect format for placeholders', () {
        expect(
          () {
            setupLocalizations(<String, String>{
              'en': '''
{
  "helloWorlds": "{count,plural, =0{Hello}=1{Hello World}=2{Hello two worlds}few{Hello {count} worlds}many{Hello all {count} worlds}other{Hello other {count} worlds}}",
  "@helloWorlds": {
    "placeholders": "Incorrectly a string, should be a map."
  }
}'''
            });
          },
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            allOf(
              contains('is not properly formatted'),
              contains('Ensure that it is a map with string valued keys'),
            ),
          )),
        );
      });
    });

    group('select messages', () {
      testWithoutContext('should automatically infer select placeholders that are not explicitly defined', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "genderSelect": "{gender, select, female {She} male {He} other {they} }",
  "@genderSelect": {
    "description": "Improperly formatted since it has no placeholder attribute."
  }
}'''
        });
        expect(getGeneratedFileContent(locale: 'en'), contains('String genderSelect(String gender) {'));
      });

      testWithoutContext('should throw attempting to generate a select message with incorrect format for placeholders', () {
        expect(
          () {
            setupLocalizations(<String, String>{
              'en': '''
{
  "genderSelect": "{gender, select, female {She} male {He} other {they} }",
  "@genderSelect": {
    "placeholders": "Incorrectly a string, should be a map."
  }
}'''
            });
          },
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            allOf(
              contains('is not properly formatted'),
              contains('Ensure that it is a map with string valued keys'),
            ),
          )),
        );
      });

      testWithoutContext('should throw attempting to generate a select message with an incorrect message', () {
        try {
          setupLocalizations(<String, String>{
            'en': '''
{
  "genderSelect": "{gender, select,}",
  "@genderSelect": {
    "placeholders": {
      "gender": {}
    }
  }
}'''
          });
        } on L10nException {
          expect(logger.errorText, contains('''
[app_en.arb:genderSelect] ICU Syntax Error: Select expressions must have an "other" case.
    {gender, select,}
                    ^''')
          );
        }
      });
    });

    group('argument messages', () {
      testWithoutContext('should generate proper calls to intl.DateFormat', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "datetime": "{today, date, ::yMd}"
}'''
        });
        expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.yMd(localeName).format(today)'));
      });

      testWithoutContext('should generate proper calls to intl.DateFormat when using time', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "datetime": "{current, time, ::jms}"
}'''
        });
        expect(getGeneratedFileContent(locale: 'en'), contains('intl.DateFormat.jms(localeName).format(current)'));
      });

      testWithoutContext('should not complain when placeholders are explicitly typed to DateTime', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "datetime": "{today, date, ::yMd}",
  "@datetime": {
    "placeholders": {
      "today": { "type": "DateTime" }
    }
  }
}'''
        });
        expect(getGeneratedFileContent(locale: 'en'), contains('String datetime(DateTime today) {'));
      });

      testWithoutContext('should automatically infer date time placeholders that are not explicitly defined', () {
        setupLocalizations(<String, String>{
          'en': '''
{
  "datetime": "{today, date, ::yMd}"
}'''
        });
        expect(getGeneratedFileContent(locale: 'en'), contains('String datetime(DateTime today) {'));
      });

      testWithoutContext('should throw on invalid DateFormat', () {
        try {
          setupLocalizations(<String, String>{
            'en': '''
{
  "datetime": "{today, date, ::yMMMMMd}"
}'''
          });
          assert(false);
        } on L10nException {
          expect(logger.errorText, contains('Date format "yMMMMMd" for placeholder today does not have a corresponding DateFormat constructor'));
        }
      });
    });

    // All error handling for messages should collect errors on a per-error
    // basis and log them out individually. Then, it will throw an L10nException.
    group('error handling tests', () {
      testWithoutContext('syntax/code-gen errors properly logs errors per message', () {
        // TODO(thkim1011): Fix error handling so that long indents don't get truncated.
        // See https://github.com/flutter/flutter/issues/120490.
        try {
          setupLocalizations(<String, String>{
            'en': '''
{
  "hello": "Hello { name",
  "plural": "This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}",
  "explanationWithLexingError": "The 'string above is incorrect as it forgets to close the brace",
  "pluralWithInvalidCase": "{ count, plural, woohoo{huh?} other{lol} }"
}'''
          }, useEscaping: true);
        } on L10nException {
          expect(logger.errorText, contains('''
[app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
    Hello { name
                 ^
[app_en.arb:plural] ICU Syntax Error: Expected "}" but found no tokens.
    This is an incorrectly formatted plural: { count, plural, zero{No frog} one{One frog} other{{count} frogs}
                                                                                          ^
[app_en.arb:explanationWithLexingError] ICU Lexing Error: Unmatched single quotes.
    The 'string above is incorrect as it forgets to close the brace
        ^
[app_en.arb:pluralWithInvalidCase] ICU Syntax Error: Plural expressions case must be one of "zero", "one", "two", "few", "many", or "other".
    { count, plural, woohoo{huh?} other{lol} }
                     ^'''));
        }
      });

      testWithoutContext('errors thrown in multiple languages are all shown', () {
        try {
          setupLocalizations(<String, String>{
            'en': '{ "hello": "Hello { name" }',
            'es': '{ "hello": "Hola { name" }',
          });
        } on L10nException {
          expect(logger.errorText, contains('''
[app_en.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
    Hello { name
                 ^
[app_es.arb:hello] ICU Syntax Error: Expected "}" but found no tokens.
    Hola { name
                ^'''));
        }
      });
    });


    testWithoutContext('intl package import should be kept in subclass files when plurals are included', () {
      const String pluralMessageArb = '''
{
  "helloWorlds": "{count,plural, =0{Hello} =1{Hello World} =2{Hello two worlds} few{Hello {count} worlds} many{Hello all {count} worlds} other{Hello other {count} worlds}}",
  "@helloWorlds": {
    "description": "A plural message",
    "placeholders": {
      "count": {}
    }
  }
}
''';
      const String pluralMessageEsArb = '''
{
  "helloWorlds": "{count,plural, =0{ES - Hello} =1{ES - Hello World} =2{ES - Hello two worlds} few{ES - Hello {count} worlds} many{ES - Hello all {count} worlds} other{ES - Hello other {count} worlds}}"
}
''';
      setupLocalizations(<String, String>{
        'en': pluralMessageArb,
        'es': pluralMessageEsArb,
      });
      expect(getGeneratedFileContent(locale: 'en'), contains(intlImportDartCode));
      expect(getGeneratedFileContent(locale: 'es'), contains(intlImportDartCode));
    });

    testWithoutContext('intl package import should be kept in subclass files when select is included', () {
      const String selectMessageArb = '''
{
  "genderSelect": "{gender, select, female {She} male {He} other {they} }",
  "@genderSelect": {
    "description": "A select message",
    "placeholders": {
      "gender": {}
    }
  }
}
''';
      const String selectMessageEsArb = '''
{
  "genderSelect": "{gender, select, female {ES - She} male {ES - He} other {ES - they} }"
}
''';
      setupLocalizations(<String, String>{
        'en': selectMessageArb,
        'es': selectMessageEsArb,
      });
      expect(getGeneratedFileContent(locale: 'en'), contains(intlImportDartCode));
      expect(getGeneratedFileContent(locale: 'es'), contains(intlImportDartCode));
    });

    testWithoutContext('check indentation on generated files', () {
      setupLocalizations(<String, String>{
        'en': singleMessageArbFileString,
        'es': singleEsMessageArbFileString,
      });
      // Tests a few of the lines in the generated code.
      // Localizations lookup code
      final String localizationsFile = getGeneratedFileContent();
      expect(localizationsFile.contains('  switch (locale.languageCode) {'), true);
      expect(localizationsFile.contains("    case 'en': return AppLocalizationsEn();"), true);
      expect(localizationsFile.contains("    case 'es': return AppLocalizationsEs();"), true);
      expect(localizationsFile.contains('  }'), true);

      // Supported locales list
      expect(localizationsFile.contains('  static const List<Locale> supportedLocales = <Locale>['), true);
      expect(localizationsFile.contains("    Locale('en'),"), true);
      expect(localizationsFile.contains("    Locale('es')"), true);
      expect(localizationsFile.contains('  ];'), true);
    });

    testWithoutContext('foundation package import should be omitted from file template when deferred loading = true', () {
      setupLocalizations(<String, String>{
        'en': singleMessageArbFileString,
        'es': singleEsMessageArbFileString,
      }, useDeferredLoading: true);
      expect(getGeneratedFileContent(), isNot(contains(foundationImportDartCode)));
    });

    testWithoutContext('foundation package import should be kept in file template when deferred loading = false', () {
      setupLocalizations(<String, String>{
        'en': singleMessageArbFileString,
        'es': singleEsMessageArbFileString,
      });
      expect(getGeneratedFileContent(), contains(foundationImportDartCode));
    });

    testWithoutContext('check for string interpolation rules', () {
      const String enArbCheckList = '''
{
  "one": "The number of {one} elapsed is: 44",
  "@one": {
    "description": "test one",
    "placeholders": {
      "one": {
        "type": "String"
      }
    }
  },
  "two": "哈{two}哈",
  "@two": {
    "description": "test two",
    "placeholders": {
      "two": {
        "type": "String"
      }
    }
  },
  "three": "m{three}m",
  "@three": {
    "description": "test three",
    "placeholders": {
      "three": {
        "type": "String"
      }
    }
  },
  "four": "I have to work _{four}_ sometimes.",
  "@four": {
    "description": "test four",
    "placeholders": {
      "four": {
        "type": "String"
      }
    }
  },
  "five": "{five} elapsed.",
  "@five": {
    "description": "test five",
    "placeholders": {
      "five": {
        "type": "String"
      }
    }
  },
  "six": "{six}m",
  "@six": {
    "description": "test six",
    "placeholders": {
      "six": {
        "type": "String"
      }
    }
  },
  "seven": "hours elapsed: {seven}",
  "@seven": {
    "description": "test seven",
    "placeholders": {
      "seven": {
        "type": "String"
      }
    }
  },
  "eight": " {eight}",
  "@eight": {
    "description": "test eight",
    "placeholders": {
      "eight": {
        "type": "String"
      }
    }
  },
  "nine": "m{nine}",
  "@nine": {
    "description": "test nine",
    "placeholders": {
      "nine": {
        "type": "String"
      }
    }
  }
}
''';

      // It's fine that the arb is identical -- Just checking
      // generated code for use of '${variable}' vs '$variable'
      const String esArbCheckList = '''
{
  "one": "The number of {one} elapsed is: 44",
  "two": "哈{two}哈",
  "three": "m{three}m",
  "four": "I have to work _{four}_ sometimes.",
  "five": "{five} elapsed.",
  "six": "{six}m",
  "seven": "hours elapsed: {seven}",
  "eight": " {eight}",
  "nine": "m{nine}"
}
''';
      setupLocalizations(<String, String>{
        'en': enArbCheckList,
        'es': esArbCheckList,
      });
      final String localizationsFile = getGeneratedFileContent(locale: 'es');
      expect(localizationsFile, contains(r'$one'));
      expect(localizationsFile, contains(r'$two'));
      expect(localizationsFile, contains(r'${three}'));
      expect(localizationsFile, contains(r'${four}'));
      expect(localizationsFile, contains(r'$five'));
      expect(localizationsFile, contains(r'${six}m'));
      expect(localizationsFile, contains(r'$seven'));
      expect(localizationsFile, contains(r'$eight'));
      expect(localizationsFile, contains(r'$nine'));
    });

    testWithoutContext('check for string interpolation rules - plurals', () {
      const String enArbCheckList = '''
{
  "first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}}",
  "@first": {
    "description": "First set of plural messages to test.",
    "placeholders": {
      "count": {}
    }
  },
  "second": "{count,plural, =0{test {count}} other{ {count}}}",
  "@second": {
    "description": "Second set of plural messages to test.",
    "placeholders": {
      "count": {}
    }
  },
  "third": "{total,plural, =0{test {total}} other{ {total}}}",
  "@third": {
    "description": "Third set of plural messages to test, for number.",
    "placeholders": {
      "total": {
        "type": "int",
        "format": "compactLong"
      }
    }
  }
}
''';

      // It's fine that the arb is identical -- Just checking
      // generated code for use of '${variable}' vs '$variable'
      const String esArbCheckList = '''
{
  "first": "{count,plural, =0{test {count} test} =1{哈{count}哈} =2{m{count}m} few{_{count}_} many{{count} test} other{{count}m}}",
  "second": "{count,plural, =0{test {count}} other{ {count}}}"
}
''';
      setupLocalizations(<String, String>{
        'en': enArbCheckList,
        'es': esArbCheckList,
      });
      final String localizationsFile = getGeneratedFileContent(locale: 'es');
      expect(localizationsFile, contains(r'test $count test'));
      expect(localizationsFile, contains(r'哈$count哈'));
      expect(localizationsFile, contains(r'm${count}m'));
      expect(localizationsFile, contains(r'_${count}_'));
      expect(localizationsFile, contains(r'$count test'));
      expect(localizationsFile, contains(r'${count}m'));
      expect(localizationsFile, contains(r'test $count'));
      expect(localizationsFile, contains(r' $count'));
      expect(localizationsFile, contains(r'String totalString = totalNumberFormat'));
      expect(localizationsFile, contains(r'totalString'));
      expect(localizationsFile, contains(r'totalString'));
    });

    testWithoutContext(
      'should throw with descriptive error message when failing to parse the '
      'arb file',
      () {
        const String arbFileWithTrailingComma = '''
{
  "title": "Stocks",
  "@title": {
    "description": "Title for the Stocks application"
  },
}''';
        expect(
          () {
            setupLocalizations(<String, String>{ 'en': arbFileWithTrailingComma });
          },
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            allOf(
              contains('app_en.arb'),
              contains('FormatException'),
              contains('Unexpected character'),
            ),
          )),
        );
      },
    );

    testWithoutContext('should throw when resource is missing resource attribute (isResourceAttributeRequired = true)', () {
      const String arbFileWithMissingResourceAttribute = '''
{
  "title": "Stocks"
}''';
      expect(
        () {
          setupLocalizations(
            <String, String>{ 'en': arbFileWithMissingResourceAttribute },
            areResourceAttributeRequired: true,
          );
        },
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains('Resource attribute "@title" was not found'),
        )),
      );
    });

    group('checks for method/getter formatting', () {
      testWithoutContext('cannot contain non-alphanumeric symbols', () {
        const String nonAlphaNumericArbFile = '''
{
  "title!!": "Stocks",
  "@title!!": {
    "description": "Title for the Stocks application"
  }
}''';
        expect(
          () => setupLocalizations(<String, String>{ 'en': nonAlphaNumericArbFile }),
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            contains('Invalid ARB resource name'),
          )),
        );
      });

      testWithoutContext('must start with lowercase character', () {
        const String nonAlphaNumericArbFile = '''
{
  "Title": "Stocks",
  "@Title": {
    "description": "Title for the Stocks application"
  }
}''';
        expect(
          () => setupLocalizations(<String, String>{ 'en': nonAlphaNumericArbFile }),
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            contains('Invalid ARB resource name'),
          )),
        );
      });

      testWithoutContext('cannot start with a number', () {
        const String nonAlphaNumericArbFile = '''
{
  "123title": "Stocks",
  "@123title": {
    "description": "Title for the Stocks application"
  }
}''';
        expect(
          () => setupLocalizations(<String, String>{ 'en': nonAlphaNumericArbFile }),
          throwsA(isA<L10nException>().having(
            (L10nException e) => e.message,
            'message',
            contains('Invalid ARB resource name'),
          )),
        );
      });

      testWithoutContext('can start with and contain a dollar sign', () {
        const String dollarArbFile = r'''
{
  "$title$": "Stocks",
  "@$title$": {
    "description": "Title for the application"
  }
}''';
        setupLocalizations(<String, String>{ 'en': dollarArbFile });
      });
    });

    testWithoutContext('throws when the language code is not supported', () {
      const String arbFileWithInvalidCode = '''
{
  "@@locale": "invalid",
  "title": "invalid"
}''';

      final Directory l10nDirectory = fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
        ..createSync(recursive: true);
      l10nDirectory.childFile('app_invalid.arb')
        .writeAsStringSync(arbFileWithInvalidCode);

      expect(
        () {
          LocalizationsGenerator(
            fileSystem: fs,
            inputPathString: defaultL10nPathString,
            outputPathString: defaultL10nPathString,
            templateArbFileName: 'app_invalid.arb',
            outputFileString: defaultOutputFileString,
            classNameString: defaultClassNameString,
            logger: logger,
          )
            ..loadResources()
            ..writeOutputFiles();
        },
        throwsA(isA<L10nException>().having(
          (L10nException e) => e.message,
          'message',
          contains('"invalid" is not a supported language code.'),
        )),
      );
    });
  });

  testWithoutContext('should generate a valid pubspec.yaml file when using synthetic package if it does not already exist', () {
    setupLocalizations(<String, String>{
      'en': singleMessageArbFileString,
    });
    final Directory outputDirectory = fs.directory(syntheticPackagePath);
    final File pubspecFile = outputDirectory.childFile('pubspec.yaml');
    expect(pubspecFile.existsSync(), isTrue);

    final YamlNode yamlNode = loadYamlNode(pubspecFile.readAsStringSync());
    expect(yamlNode, isA<YamlMap>());

    final YamlMap yamlMap = yamlNode as YamlMap;
    final String pubspecName = yamlMap['name'] as String;
    final String pubspecDescription = yamlMap['description'] as String;
    expect(pubspecName, 'synthetic_package');
    expect(pubspecDescription, "The Flutter application's synthetic package.");
  });

  testWithoutContext('should not overwrite existing pubspec.yaml file when using synthetic package', () {
    final File pubspecFile = fs.file(fs.path.join(syntheticPackagePath, 'pubspec.yaml'))
      ..createSync(recursive: true)
      ..writeAsStringSync('abcd');
    setupLocalizations(<String, String>{
      'en': singleMessageArbFileString,
    });
    // The original pubspec file should not be overwritten.
    expect(pubspecFile.readAsStringSync(), 'abcd');
  });

  testWithoutContext('can use type: int without specifying a format', () {
    const String arbFile = '''
{
  "orderNumber": "This is order #{number}.",
  "@orderNumber": {
    "description": "The title for an order with a given number.",
    "placeholders": {
      "number": {
        "type": "int"
      }
    }
  }
}''';
    setupLocalizations(<String, String>{
      'en': arbFile,
    });
    expect(getGeneratedFileContent(locale: 'en'), containsIgnoringWhitespace(r'''
String orderNumber(int number) {
  return 'This is order #$number.';
}
'''));
    expect(getGeneratedFileContent(locale: 'en'), isNot(contains(intlImportDartCode)));
  });

  testWithoutContext('app localizations lookup is a public method', () {
    setupLocalizations(<String, String>{ 'en': singleMessageArbFileString });
    expect(getGeneratedFileContent(), containsIgnoringWhitespace(r'''
AppLocalizations lookupAppLocalizations(Locale locale) {
'''));
  });

  testWithoutContext('escaping with single quotes', () {
    const String arbFile = '''
{
  "singleQuote": "Flutter''s amazing!",
  "@singleQuote": {
    "description": "A message with a single quote."
  }
}''';
    setupLocalizations(<String, String>{ 'en': arbFile }, useEscaping: true);
    expect(getGeneratedFileContent(locale: 'en'), contains(r"Flutter\'s amazing"));
  });

  testWithoutContext('suppress warnings flag actually suppresses warnings', () {
    const String pluralMessageWithOverriddenParts = '''
{
  "helloWorlds": "{count,plural, =0{Hello}zero{hello} other{hi}}",
  "@helloWorlds": {
    "description": "Properly formatted but has redundant zero cases.",
    "placeholders": {
      "count": {}
    }
  }
}''';
    setupLocalizations(
      <String, String>{ 'en': pluralMessageWithOverriddenParts },
      suppressWarnings: true,
    );
    expect(logger.hadWarningOutput, isFalse);
  });

  testWithoutContext('can use decimalPatternDigits with decimalDigits optional parameter', () {
    const String arbFile = '''
{
  "treeHeight": "Tree height is {height}m.",
  "@treeHeight": {
    "placeholders": {
      "height": {
        "type": "double",
        "format": "decimalPatternDigits",
        "optionalParameters": {
          "decimalDigits": 3
        }
      }
    }
  }
}''';
    setupLocalizations(<String, String>{ 'en': arbFile });
    final String localizationsFile = getGeneratedFileContent(locale: 'en');
    expect(localizationsFile, containsIgnoringWhitespace(r'''
String treeHeight(double height) {
'''));
    expect(localizationsFile, containsIgnoringWhitespace(r'''
NumberFormat.decimalPatternDigits(
  locale: localeName,
  decimalDigits: 3
);
'''));
  });

  // Regression test for https://github.com/flutter/flutter/issues/125461.
  testWithoutContext('dollar signs are escaped properly when there is a select clause', () {
    const String dollarSignWithSelect = r'''
{
  "dollarSignWithSelect": "$nice_bug\nHello Bug! Manifistation #1 {selectPlaceholder, select, case{message} other{messageOther}}"
}''';
    setupLocalizations(<String, String>{ 'en': dollarSignWithSelect });
    expect(getGeneratedFileContent(locale: 'en'), contains(r'\$nice_bug\nHello Bug! Manifistation #1 $_temp0'));
  });
}