// Copyright (c) 2018, 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.

import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analyzer/src/test_utilities/platform.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../tool/lsp_spec/matchers.dart';
import '../utils/matchers.dart';
import '../utils/test_code_extensions.dart';
import 'server_abstract.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(FormatTest);
  });
}

@reflectiveTest
class FormatTest extends AbstractLspAnalysisServerTest {
  /// Some sample code that is over 50 characters and will be wrapped if the
  /// page width is set to 40.
  static const codeThatWrapsAt40 =
      "var a = '        10        20        30        40';";

  /// Verifies the result of a format-on-type request for the character in the
  /// marked range in [content].
  ///
  /// If the marked range has a position, it will be used for the request,
  /// otherwise the end point of the range will be used.
  Future<List<TextEdit>?> expectFormatOnTypeResult(
    String content,
    Matcher matcher,
  ) async {
    var code = TestCode.parseNormalized(content);

    // We use range to make it really explicit which character is typed and not
    // have to worry about whether it's before or after the ^.
    expect(code.ranges, hasLength(1));
    expect(code.positions, hasLength(anyOf(0, 1)));

    var typedCharacter = code.code.substring(
      code.range.sourceRange.offset,
      code.range.sourceRange.end,
    );
    await initialize();
    await openFile(mainFileUri, code.code);

    var result = await formatOnType(
      mainFileUri,
      code.positions.isNotEmpty ? code.position.position : code.range.range.end,
      typedCharacter,
    );

    expect(
      result,
      matcher,
      reason:
          'Typing the character "$typedCharacter" at the marked location '
          'should match the supplied matcher',
    );

    return result;
  }

  Future<List<TextEdit>> expectFormattedContents(
    Uri uri,
    String original,
    String expected,
  ) async {
    var formatEdits = (await formatDocument(uri))!;
    var formattedContents = applyTextEdits(original, formatEdits);
    expect(formattedContents, equalsNormalized(expected));
    return formatEdits;
  }

  Future<List<TextEdit>> expectRangeFormattedContents(
    Uri uri,
    TestCode code,
    String expected,
  ) async {
    var formatEdits = (await formatRange(uri, code.range.range))!;
    var formattedContents = applyTextEdits(code.code, formatEdits);
    expect(formattedContents, equalsNormalized(expected));
    return formatEdits;
  }

  Future<String> formatContents(Uri uri, String original) async {
    var formatEdits = (await formatDocument(uri))!;
    return applyTextEdits(original, formatEdits);
  }

  Future<void> test_alreadyFormatted() async {
    var code = TestCode.parseNormalized('''
void f() {
  print('test');
}
''');
    await initialize();
    await openFile(mainFileUri, code.code);

    var formatEdits = await formatDocument(mainFileUri);
    expect(formatEdits, isNull);
  }

  /// Formatting a file with '\r\n' and then a file with '\n' should not result
  /// in '\r' being added to the second file.
  Future<void> test_changedLineEndings() async {
    await provideConfig(
      initialize,
      // The bug only occurred with an explicit line length because reuse
      // of the formatter accidentally required lineLength match the formatters
      // (non-null)  line length.
      {'lineLength': 80},
    );

    // First format the doc with '\r\n'.
    await openFile(mainFileUri, 'int? a;\r\n');
    await formatDocument(mainFileUri);

    // Now replace and format with '\n'.
    await replaceFile(2, mainFileUri, 'int? a;\n');
    var formatEdits = await formatDocument(mainFileUri);

    // Expect no edits because this document was already formatted.
    // When the bug occurs, we'd see edits to add a '\r'.
    expect(formatEdits, isNull);
  }

  Future<void> test_complex() async {
    failTestOnErrorDiagnostic = false;

    var code = TestCode.parseNormalized('''
ErrorOr<Pair<A, List<B>>> c(
  String d,
  List<
          Either2<E,
              F>>
      g, {
  h = false,
}) {
}


    ''');
    var expected = '''
ErrorOr<Pair<A, List<B>>> c(String d, List<Either2<E, F>> g, {h = false}) {}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    await expectFormattedContents(mainFileUri, code.code, expected);
  }

  /// Ensures we use the same registration ID when unregistering even if the
  /// server has regenerated registrations multiple times.
  Future<void> test_dynamicRegistration_correctIdAfterMultipleChanges() async {
    setDocumentFormattingDynamicRegistration();
    setDidChangeConfigurationDynamicRegistration();

    var registrations = <Registration>[];
    // Provide empty config and collect dynamic registrations during
    // initialization.
    await provideConfig(
      () => monitorDynamicRegistrations(registrations, initialize),
      {},
    );

    Registration? registration(Method method) =>
        registrationFor(registrations, method);

    // By default, the formatters should have been registered.
    expect(registration(Method.textDocument_formatting), isNotNull);
    expect(registration(Method.textDocument_onTypeFormatting), isNotNull);
    expect(registration(Method.textDocument_rangeFormatting), isNotNull);

    // Sending config updates causes the server to rebuild its list of registrations
    // which exposes a previous bug where we'd retain newly-built registrations
    // that may not have been sent to the client (because they had previously
    // been sent), resulting in the wrong ID being used for unregistration.
    await updateConfig({'foo1': true});
    await updateConfig({'foo1': null});

    // They should be unregistered if we change the config to disabled.
    await monitorDynamicUnregistrations(
      registrations,
      () => updateConfig({'enableSdkFormatter': false}),
    );
    expect(registration(Method.textDocument_formatting), isNull);
    expect(registration(Method.textDocument_onTypeFormatting), isNull);
    expect(registration(Method.textDocument_rangeFormatting), isNull);
  }

  Future<void> test_dynamicRegistration_forConfiguration() async {
    setDocumentFormattingDynamicRegistration();
    setDidChangeConfigurationDynamicRegistration();

    var registrations = <Registration>[];
    // Provide empty config and collect dynamic registrations during
    // initialization.
    await provideConfig(
      () => monitorDynamicRegistrations(registrations, initialize),
      {},
    );

    Registration? registration(Method method) =>
        registrationFor(registrations, method);

    // By default, the formatters should have been registered.
    expect(registration(Method.textDocument_formatting), isNotNull);
    expect(registration(Method.textDocument_onTypeFormatting), isNotNull);
    expect(registration(Method.textDocument_rangeFormatting), isNotNull);

    // They should be unregistered if we change the config to disabled.
    await monitorDynamicUnregistrations(
      registrations,
      () => updateConfig({'enableSdkFormatter': false}),
    );
    expect(registration(Method.textDocument_formatting), isNull);
    expect(registration(Method.textDocument_onTypeFormatting), isNull);
    expect(registration(Method.textDocument_rangeFormatting), isNull);

    // They should be reregistered if we change the config to enabled.
    await monitorDynamicRegistrations(
      registrations,
      () => updateConfig({'enableSdkFormatter': true}),
    );
    expect(registration(Method.textDocument_formatting), isNotNull);
    expect(registration(Method.textDocument_onTypeFormatting), isNotNull);
    expect(registration(Method.textDocument_rangeFormatting), isNotNull);
  }

  Future<void> test_formatOnType_bad_brace_inComment() async {
    await expectFormatOnTypeResult('''
void f  ()
{
    // hello[!}!]
    print('test');
}
    ''', isNull);
  }

  Future<void> test_formatOnType_bad_brace_inString() async {
    await expectFormatOnTypeResult('''
void f  ()
{

    print('te[!}!]st');
}
    ''', isNull);
  }

  /// Ensure the semicolon after enum constants does not trigger formatting
  /// because the formatter removes it if there is nothing after it.
  ///
  /// https://github.com/dart-lang/sdk/issues/61909
  Future<void> test_formatOnType_bad_semiColon_enumBodySeparator() async {
    await expectFormatOnTypeResult('''
enum E { a, b[!;!] }
    ''', isNull);
  }

  Future<void> test_formatOnType_bad_semicolon_inComment() async {
    await expectFormatOnTypeResult('''
void f  ()
{
    // hello[!;!]
    print('test');
}
    ''', isNull);
  }

  Future<void> test_formatOnType_bad_semicolon_inString() async {
    await expectFormatOnTypeResult('''
void f  ()
{

    print('te[!;!]st');
}
    ''', isNull);
  }

  Future<void> test_formatOnType_good_brace_blockEnd() async {
    await expectFormatOnTypeResult('''
void f  ()
{
    print('test');
[!}!]
    ''', isNotNull);
  }

  Future<void> test_formatOnType_good_formatsCode() async {
    var code = TestCode.parseNormalized('''
void f  ()
{

    print('test');
[!}!]
    ''');
    var expected = '''void f() {
  print('test');
}
''';
    var formatEdits = (await expectFormatOnTypeResult(
      code.markedCode,
      isNotNull,
    ))!;
    var formattedContents = applyTextEdits(code.code, formatEdits);
    expect(formattedContents, equalsNormalized(expected));
  }

  Future<void> test_formatOnType_good_positionAtEnd() async {
    await expectFormatOnTypeResult('''
void f  ()
{
[!}^!]
    ''', isNotNull);
  }

  Future<void> test_formatOnType_good_positionAtOffset() async {
    await expectFormatOnTypeResult('''
void f  ()
{
[!^}!]
    ''', isNotNull);
  }

  Future<void> test_formatOnType_good_semiColon_enumConstructor() async {
    await expectFormatOnTypeResult('''
enum X {
  a,
  b;

  const X()[!;!]
}
    ''', isNotNull);
  }

  Future<void> test_formatOnType_good_semicolon_statementEnd() async {
    await expectFormatOnTypeResult('''
void f  ()
{
    print('test')[!;!]
}
    ''', isNotNull);
  }

  Future<void> test_formatRange_editsOverlapRange() async {
    // Only ranges that are fully contained by the range should be applied,
    // not those that intersect the start/end.
    var code = TestCode.parseNormalized('''
void f()
{
    [!    print('test');
        print('test');
    !]    print('test');
}
''');
    var expected = '''
void f()
{
        print('test');
  print('test');
        print('test');
}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    await expectRangeFormattedContents(mainFileUri, code, expected);
  }

  Future<void> test_formatRange_expandsLeadingWhitespaceToNearestLine() async {
    var code = TestCode.parseNormalized('''
void f()
{

[!        print('test'); // line 2
        print('test'); // line 3
        print('test'); // line 4!]
}
''');
    const expected = '''
void f()
{

  print('test'); // line 2
  print('test'); // line 3
  print('test'); // line 4
}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    await expectRangeFormattedContents(mainFileUri, code, expected);
  }

  Future<void> test_formatRange_invalidRange() async {
    var code = TestCode.parseNormalized('''
void f()
{
        print('test');
}
''');
    await initialize();
    await openFile(mainFileUri, code.code);
    var formatRangeRequest = formatRange(
      mainFileUri,
      Range(
        start: Position(line: 0, character: 0),
        end: Position(line: 10000, character: 0),
      ),
    );
    await expectLater(
      formatRangeRequest,
      throwsA(isResponseError(ServerErrorCodes.invalidFileLineCol)),
    );
  }

  Future<void> test_formatRange_simple() async {
    var code = TestCode.parseNormalized('''
main  ()
{

    print('test');
}

[!main2  ()
{

    print('test');
}!]

main3  ()
{

    print('test');
}
''');
    var expected = '''
main  ()
{

    print('test');
}

main2() {
  print('test');
}

main3  ()
{

    print('test');
}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    await expectRangeFormattedContents(mainFileUri, code, expected);
  }

  Future<void> test_formatRange_trailingNewline_47702() async {
    // Check we complete when a formatted block ends with a newline.
    // https://github.com/dart-lang/sdk/issues/47702
    var code = TestCode.parseNormalized('''
int? a;
[!
    int? b;
!]
''');
    var expected = '''
int? a;

int? b;

''';
    await initialize();
    await openFile(mainFileUri, code.code);
    await expectRangeFormattedContents(mainFileUri, code, expected);
  }

  Future<void> test_invalidSyntax() async {
    failTestOnErrorDiagnostic = false;

    var code = TestCode.parseNormalized('''
void f(((( {
  print('test');
}
''');
    await initialize();
    await openFile(mainFileUri, code.code);

    var formatEdits = await formatDocument(mainFileUri);
    expect(formatEdits, isNull);
  }

  Future<void> test_lineLength() async {
    var code = TestCode.parseNormalized('''
void f() =>
print(
'123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789'
);
    ''');
    var expectedDefault = '''
void f() => print(
  '123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789',
);
''';
    var expectedLongLines = '''
void f() => print('123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789');
''';

    // Initialize with config support, supplying an empty config when requested.
    await provideConfig(
      initialize,
      {}, // empty config
    );
    await openFile(mainFileUri, code.code);

    await expectFormattedContents(mainFileUri, code.code, expectedDefault);
    await updateConfig({'lineLength': 500});
    await expectFormattedContents(mainFileUri, code.code, expectedLongLines);
  }

  Future<void> test_lineLength_analysisOptions() async {
    const codeContent = codeThatWrapsAt40;
    const optionsContent = '''
formatter:
  page_width: 40
''';

    newFile(analysisOptionsPath, optionsContent);
    newFile(mainFilePath, codeContent);
    await initialize();

    // Ignore trailing newlines when checking for wrapping.
    var formatted = await formatContents(mainFileUri, codeContent);
    expect(formatted.trim(), contains('\n'));
  }

  Future<void> test_lineLength_analysisOptions_nestedEmpty() async {
    const codeContent = codeThatWrapsAt40;
    const optionsContent = '''
formatter:
  page_width: 40
''';
    var nestedAnalysisOptionsPath = join(
      projectFolderPath,
      'lib',
      'analysis_options.yaml',
    );

    newFile(analysisOptionsPath, optionsContent);
    newFile(
      nestedAnalysisOptionsPath,
      '# empty',
    ); // suppress the parent options.
    newFile(mainFilePath, codeContent);
    await initialize();

    // Ignore trailing newlines when checking for wrapping.
    var formatted = await formatContents(mainFileUri, codeContent);
    expect(formatted.trim(), isNot(contains('\n')));
  }

  Future<void> test_lineLength_analysisOptions_nestedIncludes() async {
    const codeContent = codeThatWrapsAt40;
    const optionsContent = '''
formatter:
  page_width: 40
''';
    var nestedAnalysisOptionsPath = join(
      projectFolderPath,
      'lib',
      'analysis_options.yaml',
    );

    newFile(analysisOptionsPath, optionsContent);
    newFile(nestedAnalysisOptionsPath, 'include: ../analysis_options.yaml');
    newFile(mainFilePath, codeContent);
    await initialize();

    // Ignore trailing newlines when checking for wrapping.
    var formatted = await formatContents(mainFileUri, codeContent);
    expect(formatted.trim(), contains('\n'));
  }

  Future<void> test_lineLength_analysisOptions_overridesConfig() async {
    const codeContent = codeThatWrapsAt40;
    const optionsContent = '''
formatter:
  page_width: 40
''';

    newFile(analysisOptionsPath, optionsContent);
    newFile(mainFilePath, codeContent);
    await provideConfig(initialize, {
      // This won't apply because analysis_options wins.
      'lineLength': 100,
    });

    // Ignore trailing newlines when checking for wrapping.
    var formatted = await formatContents(mainFileUri, codeContent);
    expect(formatted.trim(), contains('\n'));
  }

  Future<void> test_lineLength_outsideWorkspaceFolders() async {
    var code = TestCode.parseNormalized('''
void f() {
  print('123456789 ''123456789 ''123456789 ');
}
''');
    const expectedContents = '''
void f() {
  print(
    '123456789 '
    '123456789 '
    '123456789 ',
  );
}
''';

    await provideConfig(
      () => initialize(
        // Use empty roots so the test file is not inside any known
        // WorkspaceFolder.
        allowEmptyRootUri: true,
      ),
      // Global config (this should be used).
      {'lineLength': 10},
    );
    await openFile(mainFileUri, code.code);
    await expectFormattedContents(mainFileUri, code.code, expectedContents);
  }

  Future<void> test_lineLength_workspaceFolderSpecified() async {
    var code = TestCode.parseNormalized('''
void f() {
  print('123456789 ''123456789 ''123456789 ');
}
''');
    const expectedContents = '''
void f() {
  print(
    '123456789 '
    '123456789 '
    '123456789 ',
  );
}
''';

    await provideConfig(
      initialize,
      // Global config.
      {'lineLength': 200},
      folderConfig: {
        // WorkspaceFolder config for this project (this should be used).
        projectFolderPath: {'lineLength': 10},
      },
    );
    await openFile(mainFileUri, code.code);
    await expectFormattedContents(mainFileUri, code.code, expectedContents);
  }

  Future<void> test_lineLength_workspaceFolderUnspecified() async {
    var code = TestCode.parseNormalized('''
void f() {
  print('123456789 ''123456789 ''123456789 ');
}
''');
    const expectedContents = '''
void f() {
  print(
    '123456789 '
    '123456789 '
    '123456789 ',
  );
}
''';

    await provideConfig(
      initialize,
      // Global config (this should be used).
      {'lineLength': 10},
      folderConfig: {
        // WorkspaceFolder config for this project that doesn't specific
        // lineLength.
        projectFolderPath: {'someOtherValue': 'foo'},
      },
    );
    await openFile(mainFileUri, code.code);
    await expectFormattedContents(mainFileUri, code.code, expectedContents);
  }

  Future<void> test_minimalEdits_addWhitespace() async {
    // Check we only get one edit to add the required whitespace and not
    // an entire document replacement.
    var code = TestCode.parseNormalized('''
void f(){}
''');
    const expected = '''
void f() {}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    var formatEdits = await expectFormattedContents(
      mainFileUri,
      code.code,
      expected,
    );
    expect(formatEdits, hasLength(1));
    expect(formatEdits[0].newText, ' ');
    expect(formatEdits[0].range.start, equals(Position(line: 0, character: 8)));
  }

  Future<void> test_minimalEdits_removeFileLeadingWhitespace() async {
    // Check whitespace before the first token is handled.
    var code = TestCode.parseNormalized('''



void f() {}
''');
    const expected = '''
void f() {}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    var formatEdits = await expectFormattedContents(
      mainFileUri,
      code.code,
      expected,
    );
    expect(formatEdits, hasLength(1));
    expect(formatEdits[0].newText, '');
    expect(formatEdits[0].range.start, equals(Position(line: 0, character: 0)));
    expect(formatEdits[0].range.end, equals(Position(line: 3, character: 0)));
  }

  Future<void> test_minimalEdits_removeFileTrailingWhitespace() async {
    // Check whitespace after the last token is handled.
    var code = TestCode.parseNormalized('''
void f() {}




''');
    const expected = '''
void f() {}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    var formatEdits = await expectFormattedContents(
      mainFileUri,
      code.code,
      expected,
    );
    expect(formatEdits, hasLength(1));
    expect(formatEdits[0].newText, '');
    expect(formatEdits[0].range.start, equals(Position(line: 1, character: 0)));
    expect(formatEdits[0].range.end, equals(Position(line: 5, character: 0)));
  }

  Future<void> test_minimalEdits_removePartialWhitespaceAfter() async {
    // Check we get an edit only to remove the unnecessary trailing whitespace
    // and not to replace the whole whitespace with a single space.
    var code = TestCode.parseNormalized('''
void f()       {}
''');
    const expected = '''
void f() {}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    var formatEdits = await expectFormattedContents(
      mainFileUri,
      code.code,
      expected,
    );
    expect(formatEdits, hasLength(1));
    expect(
      formatEdits[0],
      equals(
        TextEdit(
          range: Range(
            start: Position(line: 0, character: 9),
            end: Position(line: 0, character: 15),
          ),
          newText: '',
        ),
      ),
    );
  }

  Future<void> test_minimalEdits_removePartialWhitespaceBefore() async {
    // Check we get an edit only to remove the unnecessary leading whitespace
    // and not to replace the whole whitespace with a single space.
    var code = TestCode.parseNormalized('''
void f()


 {}
''');
    const expected = '''
void f() {}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    var formatEdits = await expectFormattedContents(
      mainFileUri,
      code.code,
      expected,
    );
    expect(formatEdits, hasLength(1));
    expect(
      formatEdits[0],
      equals(
        TextEdit(
          range: Range(
            start: Position(line: 0, character: 8),
            end: Position(line: 3, character: 0),
          ),
          newText: '',
        ),
      ),
    );
  }

  Future<void> test_minimalEdits_removeWhitespace() async {
    // Check we only get two edits to remove the unwanted whitespace and not
    // an entire document replacement.
    var code = TestCode.parseNormalized('''
void f( ) { }
''');
    const expected = '''
void f() {}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    var formatEdits = await expectFormattedContents(
      mainFileUri,
      code.code,
      expected,
    );
    expect(formatEdits, hasLength(2));
    expect(formatEdits[0].newText, isEmpty);
    expect(formatEdits[0].range.start, equals(Position(line: 0, character: 7)));
    expect(formatEdits[1].newText, isEmpty);
    expect(
      formatEdits[1].range.start,
      equals(Position(line: 0, character: 11)),
    );
  }

  Future<void> test_minimalEdits_withComments() async {
    // Check we can get edits that span a comment (which does not appear in the
    // main token list).
    var code = TestCode.parseNormalized('''
void f() {
        var a = 1;
        // Comment
        print(a);
}
''');
    const expected = '''
void f() {
  var a = 1;
  // Comment
  print(a);
}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    var formatEdits = await expectFormattedContents(
      mainFileUri,
      code.code,
      expected,
    );
    expect(formatEdits, hasLength(3));
    expect(
      formatEdits[0],
      equals(
        TextEdit(
          range: Range(
            start: Position(line: 1, character: 2),
            end: Position(line: 1, character: 8),
          ),
          newText: '',
        ),
      ),
    );
    expect(
      formatEdits[1],
      equals(
        TextEdit(
          range: Range(
            start: Position(line: 2, character: 2),
            end: Position(line: 2, character: 8),
          ),
          newText: '',
        ),
      ),
    );
    expect(
      formatEdits[2],
      equals(
        TextEdit(
          range: Range(
            start: Position(line: 3, character: 2),
            end: Position(line: 3, character: 8),
          ),
          newText: '',
        ),
      ),
    );
  }

  Future<void> test_nonDartFile() async {
    await initialize();
    await openFile(pubspecFileUri, simplePubspecContent);

    var formatEdits = await formatOnType(pubspecFileUri, startOfDocPos, '}');
    expect(formatEdits, isNull);
  }

  Future<void> test_path_doesNotExist() async {
    await initialize();

    await expectLater(
      formatDocument(toUri(join(projectFolderPath, 'missing.dart'))),
      throwsA(
        isResponseError(
          ServerErrorCodes.invalidFilePath,
          message: 'File does not exist',
        ),
      ),
    );
  }

  Future<void> test_path_invalidFormat() async {
    await initialize();

    await expectLater(
      formatDocument(
        // Add some invalid path characters to the end of a valid file:// URI.
        Uri.parse(mainFileUri.toString() + r'###***\\\///:::.dart'),
      ),
      throwsA(
        isResponseError(
          ServerErrorCodes.invalidFilePath,
          message: 'URI does not contain a valid file path',
        ),
      ),
    );
  }

  Future<void> test_path_notFileScheme() async {
    await initialize();

    await expectLater(
      formatDocument(Uri.parse('a:/a.dart')),
      throwsA(
        isResponseError(
          ServerErrorCodes.invalidFilePath,
          message:
              "URI scheme 'a' is not supported. Allowed schemes are 'file'.",
        ),
      ),
    );
  }

  Future<void> test_simple() async {
    var code = TestCode.parseNormalized('''
    void f  ()
    {

        print('test');
    }
    ''');
    var expected = '''
void f() {
  print('test');
}
''';
    await initialize();
    await openFile(mainFileUri, code.code);
    await expectFormattedContents(mainFileUri, code.code, expected);
  }

  Future<void> test_trailingCommas_automate() async {
    const optionsContent = '''
formatter:
  trailing_commas: automate
''';
    const initialContent = '''
enum A {
  a,
  b,
}
''';
    const expectedContent = '''
enum A { a, b }
''';

    newFile(analysisOptionsPath, optionsContent);
    newFile(mainFilePath, initialContent);
    await initialize();

    // Ignore trailing newlines when checking for wrapping.
    var formatted = await formatContents(mainFileUri, initialContent);
    expect(formatted.trim(), expectedContent.trim());
  }

  Future<void> test_trailingCommas_preserve() async {
    var optionsContent = normalizeNewlinesForPlatform('''
formatter:
  trailing_commas: preserve
''');
    var initialContent = normalizeNewlinesForPlatform('''
enum A { a, b, }
''');
    var expectedContent = normalizeNewlinesForPlatform('''
enum A {
  a,
  b,
}
''');

    newFile(analysisOptionsPath, optionsContent);
    newFile(mainFilePath, initialContent);
    await initialize();

    // Ignore trailing newlines when checking for wrapping.
    var formatted = await formatContents(mainFileUri, initialContent);
    expect(formatted.trim(), expectedContent.trim());
  }

  Future<void> test_trailingCommas_unspecified() async {
    const initialContent = '''
enum A {
  a,
  b,
}
''';
    const expectedContent = '''
enum A { a, b }
''';

    newFile(mainFilePath, initialContent);
    await initialize();

    // Ignore trailing newlines when checking for wrapping.
    var formatted = await formatContents(mainFileUri, initialContent);
    expect(formatted.trim(), expectedContent.trim());
  }

  Future<void> test_unopenFile() async {
    var code = TestCode.parseNormalized('''
    void f  ()
    {

        print('test');
    }
    ''');
    var expected = '''
void f() {
  print('test');
}
''';
    newFile(mainFilePath, code.code);
    await initialize();
    await expectFormattedContents(mainFileUri, code.code, expected);
  }

  Future<void> test_validSyntax_withErrors() async {
    failTestOnErrorDiagnostic = false;

    // We should still be able to format syntactically valid code even if it has
    // analysis errors.
    var code = TestCode.parseNormalized('''
void f() {
       print(unresolved);
}
''');
    const expected = '''
void f() {
  print(unresolved);
}
''';
    await initialize();
    await openFile(mainFileUri, code.code);

    await expectFormattedContents(mainFileUri, code.code, expected);
  }
}
