// 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 'dart:async';

import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/handlers/handler_completion.dart';
import 'package:analysis_server/src/services/snippets/dart/class_declaration.dart';
import 'package:analysis_server/src/services/snippets/dart/do_statement.dart';
import 'package:analysis_server/src/services/snippets/dart/flutter_stateful_widget.dart';
import 'package:analysis_server/src/services/snippets/dart/flutter_stateful_widget_with_animation.dart';
import 'package:analysis_server/src/services/snippets/dart/flutter_stateless_widget.dart';
import 'package:analysis_server/src/services/snippets/dart/for_in_statement.dart';
import 'package:analysis_server/src/services/snippets/dart/for_statement.dart';
import 'package:analysis_server/src/services/snippets/dart/function_declaration.dart';
import 'package:analysis_server/src/services/snippets/dart/if_else_statement.dart';
import 'package:analysis_server/src/services/snippets/dart/if_statement.dart';
import 'package:analysis_server/src/services/snippets/dart/main_function.dart';
import 'package:analysis_server/src/services/snippets/dart/switch_statement.dart';
import 'package:analysis_server/src/services/snippets/dart/test_definition.dart';
import 'package:analysis_server/src/services/snippets/dart/test_group_definition.dart';
import 'package:analysis_server/src/services/snippets/dart/try_catch_statement.dart';
import 'package:analysis_server/src/services/snippets/dart/while_statement.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:collection/collection.dart';
import 'package:linter/src/lint_names.dart';
import 'package:linter/src/rules.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 'completion.dart';
import 'server_abstract.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(CompletionDocumentationResolutionTest);
    defineReflectiveTests(CompletionLabelDetailsTest);
    defineReflectiveTests(CompletionTest);
    defineReflectiveTests(DartSnippetCompletionTest);
    defineReflectiveTests(FlutterSnippetCompletionTest);
  });
}

typedef ExpectedLabel = ({
  // Main label of the completion (eg 'myFunc')
  String? label,
  // The detail part of the label (shown after label, usually truncated signature)
  String? labelDetail,
  // Additional label description (usually the auto-import URI)
  String? labelDescription,
  // Filter text (usually same as label, never with `()` or other suffixes)
  String? filterText,
  // Main detail (shown in popout, usually full signature)
  String? detail,
  // Sometimes resolved detail has a prefix added (eg. "Auto-import from").
  String? resolvedDetailPrefix,
});

abstract class AbstractCompletionTest extends AbstractLspAnalysisServerTest
    with CompletionTestMixin {
  late String content;
  late final TestCode code = TestCode.parseNormalized(content);

  new() {
    defaultInitializationOptions = {
      // Default to a high budget for tests because everything is cold and
      // may take longer to return.
      'completionBudgetMilliseconds': 50000,
    };
  }

  void expectDocumentation(CompletionItem completion, Matcher matcher) {
    var docs = completion.documentation?.map(
      (markup) => markup.value,
      (string) => string,
    );
    expect(docs, matcher);
  }

  @override
  void setUp() {
    super.setUp();

    // Completion tests have incomplete code.
    failTestOnErrorDiagnostic = false;

    setApplyEditSupport();
  }

  void _enableLints(List<String> lintNames) {
    registerLintRules();
    var lintsYaml = lintNames.map((name) => '    - $name$eol').join();
    newFile(analysisOptionsPath, '''
linter:
  rules:
$lintsYaml
''');
  }
}

@reflectiveTest
class CompletionDocumentationResolutionTest extends AbstractCompletionTest {
  Future<void> assertNoCompletionItem(String label) async {
    var completions = await getCompletion(mainFileUri, code.position.position);
    expect(completions.where((c) => c.label == label), isEmpty);
  }

  Future<CompletionItem> getCompletionItem(String label) async {
    var completions = await getCompletion(mainFileUri, code.position.position);
    return completions.singleWhere((c) => c.label == label);
  }

  Future<void> initializeServer() async {
    await initialize();
    await openFile(mainFileUri, code.code);
    await initialAnalysis;
  }

  Future<void> test_abstract_class() async {
    newFile(join(projectFolderPath, 'my_class.dart'), '''
typedef MyClass2 = MyClass;

abstract class MyClass {}
''');

    content = '''
void f() {
  MyClas^
}
''';

    await initializeServer();

    var completion = await getCompletionItem('MyClass');
    expectDocumentation(completion, isNull);
  }

  Future<void> test_class() async {
    newFile(join(projectFolderPath, 'my_class.dart'), '''
/// Class.
class MyClass {}
''');

    content = '''
void f() {
  MyClass^
}
''';

    await initializeServer();

    var completion = await getCompletionItem('MyClass');
    expectDocumentation(completion, isNull);

    var resolved = await resolveCompletion(completion);
    expectDocumentation(resolved, contains('Class.'));
  }

  Future<void> test_class_constructor() async {
    newFile(join(projectFolderPath, 'my_class.dart'), '''
class MyClass {
  /// Constructor.
  MyClass();
}
''');

    content = '''
void f() {
  MyClass^
}
''';

    await initializeServer();

    var completion = await getCompletionItem('MyClass()');
    expectDocumentation(completion, isNull);

    var resolved = await resolveCompletion(completion);
    expectDocumentation(resolved, contains('Constructor.'));
  }

  Future<void> test_class_constructor_not_duplicated() async {
    // https://github.com/dart-lang/sdk/issues/60188
    content = '''
class C {
  /// This is a constructor.
  C.c1();
}

void g(void Function() _) {}

void f() {
  g(C.^c1);
}
''';
    await initializeServer();

    var completion = await getCompletionItem('c1()');
    expectDocumentation(completion, equals('This is a constructor.'));
  }

  Future<void> test_class_constructorNamed() async {
    newFile(join(projectFolderPath, 'my_class.dart'), '''
class MyClass {
  /// Named Constructor.
  MyClass.named();
}
''');

    content = '''
void f() {
  MyClass^
}
''';

    await initializeServer();

    var completion = await getCompletionItem('MyClass.named()');
    expectDocumentation(completion, isNull);

    var resolved = await resolveCompletion(completion);
    expectDocumentation(resolved, contains('Named Constructor.'));
  }

  Future<void> test_dotShorthand_disabledFeature() async {
    content = '''
// @dart=3.9
class C {
  const C.named();
}
void f() {
  C c = const .n^();
}
''';
    // Here to make sure both insertion and completion actually replace `.`.
    setCompletionItemInsertReplaceSupport();
    await initializeServer();
    var item = await getCompletionItem('named()');
    // Usually, with `setCompletionItemInsertReplaceSupport`, we would get an
    // InsertReplaceEdit, but since dot-shorthand is disabled, we get a normal
    // TextEdit that replaces the `.` too.
    var newContent = applyTextEdits(code.code, [toTextEdit(item.textEdit!)]);
    expect(
      newContent,
      equalsNormalized('''
// @dart=3.9
class C {
  const C.named();
}
void f() {
  C c = const C.named();
}
'''),
    );
  }

  Future<void> test_enum() async {
    newFile(join(projectFolderPath, 'my_enum.dart'), '''
/// Enum.
enum MyEnum {}
''');

    content = '''
void f() {
  MyEnum^
}
''';

    await initializeServer();

    var completion = await getCompletionItem('MyEnum');
    expectDocumentation(completion, isNull);

    var resolved = await resolveCompletion(completion);
    expectDocumentation(resolved, contains('Enum.'));
  }

  Future<void> test_enum_member() async {
    // Function used to provide type context in main file without importing
    // the enum.
    newFile(join(projectFolderPath, 'lib', 'func.dart'), '''
import 'my_enum.dart';
void enumFunc(MyEnum e) {}
''');

    newFile(join(projectFolderPath, 'lib', 'my_enum.dart'), '''
enum MyEnum {
  /// Enum Member.
  one,
}
''');

    content = '''
import 'func.dart';
void f() {
  enumFunc(MyEnum^)
}
''';

    await initializeServer();

    var completion = await getCompletionItem('MyEnum.one');
    expectDocumentation(completion, isNull);

    var resolved = await resolveCompletion(completion);
    expectDocumentation(resolved, contains('Enum Member.'));
  }

  Future<void> test_enum_values() async {
    content = '''
void f() {
  print(va^)
}

enum MyEnum {
  value,
}
''';

    await initializeServer();

    await getCompletionItem('MyEnum.value');
    await assertNoCompletionItem('MyEnum.values');
    await assertNoCompletionItem('values');
  }

  Future<void> test_enum_values_insideEnum() async {
    content = '''
enum E {value}
enum MyEnum {
  value;

  void f() {
    print(^);
  }
}
''';

    await initializeServer();

    await getCompletionItem('index');
    await getCompletionItem('name');
    await getCompletionItem('value');
    await getCompletionItem('values');
    await getCompletionItem('E.value');
    await assertNoCompletionItem('MyEnum.values');
    await assertNoCompletionItem('E.values');
  }

  Future<void> test_innerPatternKeyword() async {
    content = '''
class A {
  A([this.value]);
  A? value;
}

void main() {
  final value = A(A(A()));
  if (value case A(value: A(:^))) {}
}
''';
    await initializeServer();

    var completion = await getCompletionItem('value');
    expect(completion, isNotNull);

    // Resolve the completion item to get its edits.
    var resolved = await resolveCompletion(completion);

    // Apply all current-document edits.
    var newContent = applyTextEdits(code.code, [
      toTextEdit(resolved.textEdit!),
    ]);
    expect(
      newContent,
      equalsNormalized('''
class A {
  A([this.value]);
  A? value;
}

void main() {
  final value = A(A(A()));
  if (value case A(value: A(:var value))) {}
}
'''),
    );
  }

  Future<void> test_pattern_getter_lint() async {
    content = '''
abstract class MyType {
  int get name;
}

void f(MyType m) {
  if (m case MyType(:^)) {}
}
''';
    _enableLints([LintNames.always_specify_types]);
    await initializeServer();

    var completion = await getCompletionItem('name');
    var resolved = await resolveCompletion(completion);
    var newContent = applyTextEdits(code.code, [
      toTextEdit(resolved.textEdit!),
    ]);
    expect(
      newContent,
      equalsNormalized('''
abstract class MyType {
  int get name;
}

void f(MyType m) {
  if (m case MyType(:int name)) {}
}
'''),
    );
  }

  /// We should not show `var`, `final` or the member type on the display text.
  Future<void> test_pattern_member_name_only() async {
    content = '''
void f(({int name, int other}) r) {
  if (r case (:^)) {}
}
''';
    await initializeServer();

    var completion = await getCompletionItem('name');
    expect(completion, isNotNull);

    // Resolve the completion item to get its edits.
    var resolved = await resolveCompletion(completion);

    // Apply all current-document edits.
    var newContent = applyTextEdits(code.code, [
      toTextEdit(resolved.textEdit!),
    ]);
    expect(
      newContent,
      equalsNormalized('''
void f(({int name, int other}) r) {
  if (r case (:var name)) {}
}
'''),
    );
  }

  @FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/60751')
  Future<void> test_pattern_member_type_with_hide_combinator_and_lint() async {
    newFile(join(projectFolderPath, 'lib', 'my_types.dart'), '''
class MyType {}
class MyType2 {}
typedef Other = ({MyType name,});
''');
    content = '''
import 'my_types.dart' hide MyType, MyType2;

void f(Other r) {
  if (r case (:^)) {}
}
''';
    _enableLints([LintNames.always_specify_types]);
    await initializeServer();

    var completion = await getCompletionItem('name');
    expect(completion, isNotNull);
    var resolved = await resolveCompletion(completion);
    var newContent = applyTextEdits(code.code, [
      toTextEdit(resolved.textEdit!),
    ]);
    expect(
      newContent,
      equalsNormalized('''
import 'my_types.dart' hide MyType2;

void f(Other r) {
  if (r case (:MyType name)) {}
}
'''),
    );
  }

  Future<void> test_pattern_member_type_with_new_import_and_lint() async {
    newFile(join(projectFolderPath, 'lib', 'my_types.dart'), '''
class MyType {}
''');
    newFile(join(projectFolderPath, 'lib', 'other.dart'), '''
import 'my_types.dart';
typedef Other = ({MyType name,});
''');
    content = '''
import 'other.dart';

void f(Other r) {
  if (r case (:^)) {}
}
''';
    _enableLints([LintNames.always_specify_types]);
    await initializeServer();

    var completion = await getCompletionItem('name');
    var resolved = await resolveCompletion(completion);
    var newContent = applyTextEdits(
      code.code,
      [
        toTextEdit(resolved.textEdit!),
      ].followedBy(resolved.additionalTextEdits!).toList(),
    );
    expect(
      newContent,
      equalsNormalized('''
import 'package:test/my_types.dart';

import 'other.dart';

void f(Other r) {
  if (r case (:MyType name)) {}
}
'''),
    );
  }

  @FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/60751')
  Future<void> test_pattern_member_type_with_show_combinator_and_lint() async {
    newFile(join(projectFolderPath, 'lib', 'my_types.dart'), '''
class MyType {}
typedef Other = ({MyType name,});
''');
    content = '''
import 'my_types.dart' show Other;

void f(Other r) {
  if (r case (:^)) {}
}
''';
    _enableLints([LintNames.always_specify_types]);
    await initializeServer();

    var completion = await getCompletionItem('name');
    expect(completion, isNotNull);
    var resolved = await resolveCompletion(completion);
    var newContent = applyTextEdits(code.code, [
      toTextEdit(resolved.textEdit!),
    ]);
    expect(
      newContent,
      equalsNormalized('''
import 'my_types.dart' show Other, MyType;

void f(Other r) {
  if (r case (:MyType name)) {}
}
'''),
    );
  }

  Future<void> test_pattern_type_with_aliased_import_and_lint() async {
    newFile(join(projectFolderPath, 'lib', 'my_types.dart'), '''
class MyType {}
typedef Other = ({MyType name,});
''');
    content = '''
import 'my_types.dart' as t;

void f(t.Other r) {
  if (r case (:^)) {}
}
''';
    _enableLints([LintNames.always_specify_types]);
    await initializeServer();

    var completion = await getCompletionItem('name');
    expect(completion, isNotNull);
    var resolved = await resolveCompletion(completion);
    var newContent = applyTextEdits(code.code, [
      toTextEdit(resolved.textEdit!),
    ]);
    expect(
      newContent,
      equalsNormalized('''
import 'my_types.dart' as t;

void f(t.Other r) {
  if (r case (:t.MyType name)) {}
}
'''),
    );
  }

  Future<void> test_typeAlias_constructors() async {
    newFile(join(projectFolderPath, 'lib', 'alias1.dart'), '''
import 'main.dart';
typedef Alias1 = MyClass;                // Included
typedef _AliasHidden = MyClass;          // Not included, private to another lib
''');
    newFile(join(projectFolderPath, 'lib', 'alias2.dart'), '''
import 'main.dart';
typedef Alias2 = MyClass;                // Included
''');

    content = '''
class MyClass.myConstructor();           // Included
typedef AliasLocal = MyClass;            // Included
typedef _AliasPrivateLocal = MyClass;    // Included

var id = myConstru^
''';
    await initializeServer();

    var completions = await getCompletion(mainFileUri, code.position.position);
    expect(completions, hasLength(5)); // Exactly 5, no dupes.

    /// Helper to expect a completion with [label] that has auto-import data
    /// for the URI [autoImport].
    void expectCompletion(String label, String? autoImport) {
      var completion = completions.singleWhere(
        (completion) => completion.label == label,
      );
      var info = completion.data as DartCompletionResolutionInfo?;
      var importUri = info?.importUris.singleOrNull;

      expect(importUri, autoImport ?? isNull);
    }

    expectCompletion('MyClass.myConstructor()', null);
    expectCompletion('AliasLocal.myConstructor()', null);
    expectCompletion('_AliasPrivateLocal.myConstructor()', null);
    expectCompletion('Alias1.myConstructor()', 'package:test/alias1.dart');
    expectCompletion('Alias2.myConstructor()', 'package:test/alias2.dart');
  }
}

@reflectiveTest
class CompletionLabelDetailsTest extends AbstractCompletionTest {
  late String fileAPath;

  /// A helper for [expectLabels] that only verifies a single label.
  Future<void> expectLabel(
    String content, {
    Uri? completionFileUri,
    required String? label,
    required String? labelDetail,
    required String? labelDescription,
    required String? filterText,
    required String? detail,
    String? resolvedDetailPrefix,
  }) {
    return expectLabels(
      content,
      completionFileUri: completionFileUri,
      labels: [
        (
          label: label,
          labelDetail: labelDetail,
          labelDescription: labelDescription,
          filterText: filterText,
          detail: detail,
          resolvedDetailPrefix: resolvedDetailPrefix,
        ),
      ],
    );
  }

  /// Expect that invoking completion at the position marked in [content] in
  /// [completionFileUri] provides completions with the expected [labels].
  Future<void> expectLabels(
    String content, {
    Uri? completionFileUri,
    required List<ExpectedLabel> labels,
  }) async {
    completionFileUri ??= mainFileUri;

    await initialize();
    await openFile(completionFileUri, code.code);

    var completions = await getCompletion(
      completionFileUri,
      code.position.position,
    );

    for (var (
          :label,
          :labelDetail,
          :labelDescription,
          :filterText,
          :detail,
          :resolvedDetailPrefix,
        )
        in labels) {
      var completion = completions.singleWhereOrNull(
        (c) => c.label == label && c.detail == detail,
      );
      if (completion == null) {
        fail(
          'Did not find completion "$label" with detail "$detail" in completion results:'
          '$eol    ${completions.map((c) => '${c.label} (${c.detail})').join('$eol    ')}',
        );
      }

      expect(completion.filterText, filterText);

      // If both fields are expected to be null, expect the whole object to be
      // null (to reduce payload size).
      if (labelDetail == null && labelDescription == null) {
        expect(completion.labelDetails, isNull);
      } else {
        var labelDetails = completion.labelDetails;
        if (labelDetails == null) {
          fail('Completion "$label" does not have labelDetails');
        }
        expect(labelDetails.detail, labelDetail);
        expect(labelDetails.description, labelDescription);
      }

      // Verify that resolution does not modify these results.
      var resolved = await resolveCompletion(completion);
      expect(resolved.label, completion.label);
      expect(resolved.filterText, completion.filterText);
      expect(
        resolved.detail,
        resolvedDetailPrefix != null
            ? '$resolvedDetailPrefix${completion.detail ?? ''}'
            : completion.detail,
      );
      expect(resolved.labelDetails?.detail, completion.labelDetails?.detail);
      expect(
        resolved.labelDetails?.description,
        completion.labelDetails?.description,
      );
    }
  }

  @override
  void setUp() {
    super.setUp();
    fileAPath = join(projectFolderPath, 'lib', 'a.dart');

    // TODO(dantup): Consider enabling this by default for [CompletionTest] and
    //  changing this class to test support without it (or, subclassing
    //  CompletionTest and inferring the label when labelDetails are not
    //  supported).
    setCompletionItemLabelDetailsSupport();
  }

  Future<void> test_combinator_function() async {
    content = '''
import 'dart:math' show min^
''';

    await expectLabel(
      content,
      label: 'min',
      labelDetail: ' (…) → T',
      labelDescription: null,
      filterText: null,
      detail: '(T a, T b) → T',
    );
  }

  Future<void> test_constructor_argument() async {
    content = '''
var a = Foo(^);

class Foo {
  final int value;
  const Foo({required this.value});
}
''';

    await expectLabel(
      content,
      label: 'value:',
      labelDetail: ' int',
      labelDescription: null,
      filterText: null,
      detail: 'int',
    );
  }

  Future<void> test_constructor_factory_argument() async {
    content = '''
var a = Foo(^);

class Foo {
  final int value;
  const Foo._({required this.value});
  const factory Foo({required int value}) = Foo._;
}
''';

    await expectLabel(
      content,
      label: 'value:',
      labelDetail: ' int',
      labelDescription: null,
      filterText: null,
      detail: 'int',
    );
  }

  Future<void> test_functionTearOff() async {
    content = '''
void f(void Function(int) x) {
  f(^);
}

void myFunction(int i) {}
''';

    await expectLabel(
      content,
      label: 'myFunction',
      labelDetail: ' (…) → void',
      labelDescription: null,
      filterText: null,
      detail: '(int i) → void',
    );
  }

  Future<void> test_functionType_callMember_identifierQualified() async {
    content = '''
extension on void Function(int i) {
  void Function(int i) get g2 => this.cal^;
}
''';

    await expectLabel(
      content,
      label: 'call',
      labelDetail: ' (…) → void',
      labelDescription: null,
      filterText: null,
      detail: '(int i) → void',
    );
  }

  Future<void> test_functionType_callMember_identifierUnqualified() async {
    content = '''
extension on void Function(int i) {
  void Function(int i) get g1 => cal^;
}
''';

    await expectLabel(
      content,
      label: 'call',
      labelDetail: ' (…) → void',
      labelDescription: null,
      filterText: null,
      detail: '(int i) → void',
    );
  }

  Future<void> test_functionType_callMember_invocationQualified() async {
    content = '''
extension on void Function(int i) {
  void m2() {
    this.cal^;
  }
}
''';

    await expectLabel(
      content,
      label: 'call',
      labelDetail: '(…) → void',
      labelDescription: null,
      filterText: null,
      detail: '(int i) → void',
    );
  }

  Future<void> test_functionType_callMember_invocationUnqualified() async {
    content = '''
extension on void Function(int i) {
  void m() {
    cal^;
  }
}
''';

    await expectLabel(
      content,
      label: 'call',
      labelDetail: '(…) → void',
      labelDescription: null,
      filterText: null,
      detail: '(int i) → void',
    );
  }

  Future<void> test_functionType_callMember_parameters() async {
    content = '''
void Function(int i) get g3 => g3.cal^;
''';

    await expectLabel(
      content,
      label: 'call',
      labelDetail: ' (…) → void',
      labelDescription: null,
      filterText: null,
      detail: '(int i) → void',
    );
  }

  Future<void> test_functionType_callMember_returnType() async {
    content = '''
String m3() {
  m3.cal^;
}
''';

    await expectLabel(
      content,
      label: 'call',
      labelDetail: '() → String',
      labelDescription: null,
      filterText: null,
      detail: '() → String',
    );
  }

  Future<void> test_imported_function_returnType_args() async {
    newFile(fileAPath, '''
String a(String a, {String b}) {}
''');
    content = '''
import 'a.dart';
void f() {
  a^
}
''';

    await expectLabel(
      content,
      label: 'a',
      labelDetail: '(…) → String',
      labelDescription: null,
      filterText: null,
      detail: '(String a, {String b}) → String',
    );
  }

  Future<void> test_imported_function_returnType_noArgs() async {
    newFile(fileAPath, '''
String a() {}
''');
    content = '''
import 'a.dart';
String f() {
  a^
}
''';

    await expectLabel(
      content,
      label: 'a',
      labelDetail: '() → String',
      labelDescription: null,
      filterText: null,
      detail: '() → String',
    );
  }

  Future<void> test_imported_function_void_args() async {
    newFile(fileAPath, '''
void a(String a, {String b}) {}
''');
    content = '''
import 'a.dart';
void f() {
  a^
}
''';

    await expectLabel(
      content,
      label: 'a',
      labelDetail: '(…) → void',
      labelDescription: null,
      filterText: null,
      detail: '(String a, {String b}) → void',
    );
  }

  Future<void> test_imported_function_void_noArgs() async {
    newFile(fileAPath, '''
void a() {}
''');
    content = '''
import 'a.dart';
void f() {
  a^
}
''';

    await expectLabel(
      content,
      label: 'a',
      labelDetail: '() → void',
      labelDescription: null,
      filterText: null,
      detail: '() → void',
    );
  }

  Future<void> test_local_function_returnType_args() async {
    content = '''
String f(String a, {String b}) {
  f^
}
''';
    await expectLabel(
      content,
      label: 'f',
      labelDetail: '(…) → String',
      labelDescription: null,
      filterText: null,
      detail: '(String a, {String b}) → String',
    );
  }

  Future<void> test_local_function_returnType_noArgs() async {
    content = '''
String f() {
  f^
}
''';

    await expectLabel(
      content,
      label: 'f',
      labelDetail: '() → String',
      labelDescription: null,
      filterText: null,
      detail: '() → String',
    );
  }

  Future<void> test_local_function_void_args() async {
    content = '''
void f(String a, {String b}) {
  f^
}
''';

    await expectLabel(
      content,
      label: 'f',
      labelDetail: '(…) → void',
      labelDescription: null,
      filterText: null,
      detail: '(String a, {String b}) → void',
    );
  }

  Future<void> test_local_function_void_noArgs() async {
    content = '''
void f() {
  f^
}
''';

    await expectLabel(
      content,
      label: 'f',
      labelDetail: '() → void',
      labelDescription: null,
      filterText: null,
      detail: '() → void',
    );
  }

  Future<void> test_local_getter() async {
    content = '''
String a => '';
void f() {
  a^
}
''';
    await expectLabel(
      content,
      label: 'a',
      labelDetail: '() → String',
      labelDescription: null,
      filterText: null,
      detail: '() → String',
    );
  }

  Future<void> test_local_getterAndSetter() async {
    content = '''
set a(String value) {}
String get a => '';
void f() {
  a^
}
''';
    await expectLabel(
      content,
      label: 'a',
      labelDetail: ' String',
      labelDescription: null,
      filterText: null,
      detail: 'String',
    );
  }

  Future<void> test_local_override_annotation_equals() async {
    content = '''
class Base {
}

class Derived extends Base {
  @over^
}
''';
    await expectLabel(
      content,
      label: 'override ==',
      labelDetail: '(…) → bool',
      labelDescription: null,
      filterText: null,
      detail: '(Object other) → bool',
    );
  }

  Future<void> test_local_override_annotation_method() async {
    content = '''
class Base {
  String aa(String a) => '';
}

class Derived extends Base {
  @over^
}
''';
    await expectLabel(
      content,
      label: 'override aa',
      labelDetail: '(…) → String',
      labelDescription: null,
      filterText: null,
      detail: '(String a) → String',
    );
  }

  /// Ensure overrides for getters and setters can be distinguished.
  ///
  /// https://github.com/dart-lang/sdk/issues/59929
  Future<void> test_local_override_getterAndSetter() async {
    content = '''
class A {
  int? value;
}

class B extends A {
  val^
}
''';
    await expectLabels(
      content,
      labels: [
        (
          label: 'override value',
          labelDetail: ' int? get',
          labelDescription: null,
          filterText: 'value',
          detail: 'int? get',
          resolvedDetailPrefix: null,
        ),
        (
          label: 'override value',
          labelDetail: ' set (int?)',
          labelDescription: null,
          filterText: 'value',
          detail: 'set (int?)',
          resolvedDetailPrefix: null,
        ),
      ],
    );
  }

  Future<void> test_local_override_name() async {
    content = '''
class Base {
  String aa(String a) => '';
}

class Derived extends Base {
  a^
}
''';
    await expectLabel(
      content,
      label: 'override aa',
      labelDetail: '(…) → String',
      labelDescription: null,
      filterText: 'aa',
      detail: '(String a) → String',
    );
  }

  Future<void> test_local_setter() async {
    content = '''
set a(String value) {}
void f() {
  a^
}
''';
    await expectLabel(
      content,
      label: 'a',
      labelDetail: ' String',
      labelDescription: null,
      filterText: null,
      detail: 'String',
    );
  }

  Future<void> test_notImported_function_returnType_args() async {
    newFile(fileAPath, '''
String a(String a, {String b}) {}
''');
    content = '''
void f() {
  a^
}
''';

    await expectLabel(
      content,
      label: 'a',
      labelDetail: '(…) → String',
      labelDescription: 'package:test/a.dart',
      filterText: null,
      detail: '(String a, {String b}) → String',
      resolvedDetailPrefix: "Auto import from 'package:test/a.dart'\n\n",
    );
  }

  Future<void> test_notImported_function_returnType_noArgs() async {
    newFile(fileAPath, '''
String a() {}
''');
    content = '''
String f() {
  a^
}
''';

    await expectLabel(
      content,
      label: 'a',
      labelDetail: '() → String',
      labelDescription: 'package:test/a.dart',
      filterText: null,
      detail: '() → String',
      resolvedDetailPrefix: "Auto import from 'package:test/a.dart'\n\n",
    );
  }

  Future<void> test_notImported_function_void_args() async {
    newFile(fileAPath, '''
void a(String a, {String b}) {}
''');
    content = '''
void f() {
  a^
}
''';

    await expectLabel(
      content,
      label: 'a',
      labelDetail: '(…) → void',
      labelDescription: 'package:test/a.dart',
      filterText: null,
      detail: '(String a, {String b}) → void',
      resolvedDetailPrefix: "Auto import from 'package:test/a.dart'\n\n",
    );
  }

  Future<void> test_notImported_function_void_noArgs() async {
    newFile(fileAPath, '''
void a() {}
''');
    content = '''
void f() {
  a^
}
''';

    await expectLabel(
      content,
      label: 'a',
      labelDetail: '() → void',
      labelDescription: 'package:test/a.dart',
      filterText: null,
      detail: '() → void',
      resolvedDetailPrefix: "Auto import from 'package:test/a.dart'\n\n",
    );
  }

  Future<void> test_notImported_outsideLib_relativePath() async {
    var testMainFilePath = join(projectFolderPath, 'test', 'main.dart');
    var testFileAPath = join(projectFolderPath, 'test', 'a.dart');

    newFile(testFileAPath, '''
void a(String a, {String b}) {}
''');
    content = '''
void f() {
  a^
}
''';

    await expectLabel(
      content,
      completionFileUri: toUri(testMainFilePath),
      label: 'a',
      labelDetail: '(…) → void',
      labelDescription: 'a.dart',
      filterText: null,
      detail: '(String a, {String b}) → void',
      resolvedDetailPrefix: "Auto import from 'a.dart'\n\n",
    );
  }

  Future<void> test_nullNotEmpty() async {
    content = '''
bool a = ^
''';

    /// expectLabel verifies the whole labelDetails object is null if
    /// both fields are expected to be null.
    await expectLabel(
      content,
      label: 'true',
      labelDetail: null,
      labelDescription: null,
      filterText: null,
      detail: null,
    );
  }

  Future<void> test_record() async {
    content = r'''
void f((int, int) record) {
  record.$^
}
''';

    await expectLabel(
      content,
      label: r'$1',
      labelDetail: ' int',
      labelDescription: null,
      filterText: null,
      detail: 'int',
    );
  }

  Future<void> test_variable() async {
    content = r'''
void f(int variable) {
  varia^
}
''';

    await expectLabel(
      content,
      label: 'variable',
      labelDetail: ' int',
      labelDescription: null,
      filterText: null,
      detail: 'int',
    );
  }
}

@reflectiveTest
class CompletionTest extends AbstractCompletionTest {
  /// Checks whether the correct types of documentation are returned for
  /// completions based on [preference].
  Future<void> assertDocumentation(
    String? preference, {
    required bool includesSummary,
    required bool includesFull,
  }) async {
    content = '''
/// Summary.
///
/// Full.
class A {}

A^
''';

    await provideConfig(initialize, {'documentation': ?preference});

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);
    var completion = res.singleWhere((c) => c.label == 'A');

    if (includesSummary) {
      expectDocumentation(completion, contains('Summary.'));
    } else {
      expectDocumentation(completion, isNot(contains('Summary.')));
    }

    if (includesFull) {
      expectDocumentation(completion, contains('Full.'));
    } else {
      expectDocumentation(completion, isNot(contains('Full.')));
    }
  }

  /// Checks whether the correct types of documentation are returned during
  /// `completionItem/resolve` based on [preference].
  Future<void> assertResolvedDocumentation(
    String? preference, {
    required bool includesSummary,
    required bool includesFull,
  }) async {
    newFile(join(projectFolderPath, 'other_file.dart'), '''
      /// Summary.
      ///
      /// Full.
      class InOtherFile {}
''');

    content = '''
void f() {
  InOtherF^
}
''';

    await provideConfig(initialize, {'documentation': ?preference});

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);
    var completion = res.singleWhere((c) => c.label == 'InOtherFile');

    // Expect no docs in original response and correct type of docs added
    // during resolve.
    expectDocumentation(completion, isNull);
    var resolved = await resolveCompletion(completion);

    if (includesSummary) {
      expectDocumentation(resolved, contains('Summary.'));
    } else {
      expectDocumentation(resolved, isNot(contains('Summary.')));
    }

    if (includesFull) {
      expectDocumentation(resolved, contains('Full.'));
    } else {
      expectDocumentation(resolved, isNot(contains('Full.')));
    }
  }

  Future<void> checkCompleteFunctionCallInsertText(
    String content,
    String completion, {
    required String? editText,
    InsertTextFormat? insertTextFormat,
  }) async {
    this.content = content;

    setCompletionItemSnippetSupport();
    await provideConfig(initialize, {'completeFunctionCalls': true});

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere(
      (c) => c.label == completion,
      orElse: () =>
          throw 'Did not find $completion in ${res.map((r) => r.label).toList()}',
    );

    expect(item.insertTextFormat, equals(insertTextFormat));
    // We always expect `insertText` to be `null` now, as we always use
    // `textEdit`.
    expect(item.insertText, isNull);

    // And the expected text should be in the `textEdit`.
    var textEdit = toTextEdit(item.textEdit!);
    expect(textEdit.newText, equals(editText));
    expect(textEdit.range, equals(code.range.range));
  }

  void expectAutoImportCompletion(List<CompletionItem> items, String file) {
    expect(
      items.singleWhereOrNull(
        (c) => c.detail?.contains("Auto import from '$file'") ?? false,
      ),
      isNotNull,
    );
  }

  /// Verifies a color completion with text [label] for the code [content] that
  /// includes a prefix for [colorHex] in the description.
  Future<void> expectColorCompletion(
    String content,
    String label,
    String colorHex,
  ) async {
    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var completion = res.singleWhere((c) => c.label == label);

    // Verify correct kind for color preview.
    expect(completion.kind, CompletionItemKind.Color);

    // Verify the docs are either entirely the hex code, or end with it.
    // VS Code's regex only allows the hex code at the start or end to show the
    // preview.
    var docs = completion.documentation?.map(
      (markup) => markup.value,
      (string) => string,
    );
    expect(docs, anyOf(equals(colorHex), endsWith('$eol$eol$colorHex')));
  }

  /// Expect [item] to use the default edit range, inserting the value [text].
  void expectUsesDefaultEditRange(CompletionItem item, String text) {
    expect(item.textEditText ?? item.label, text);
    expect(item.insertText, isNull);
    expect(item.textEdit, isNull);
  }

  @override
  void setUp() {
    super.setUp();
    writeTestPackageConfig(flutter: true);
  }

  Future<void> test_annotation_beforeMember() async {
    content = '''
class B {
  @^
  int a = 1;
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var completions = await getCompletion(mainFileUri, code.position.position);
    var labels = completions.map((c) => c.label).toList();
    expect(labels, contains('override'));
    expect(labels, contains('deprecated'));
    expect(labels, contains('Deprecated(…)'));
  }

  Future<void> test_annotation_endOfClass() async {
    content = '''
class B {
  @^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var completions = await getCompletion(mainFileUri, code.position.position);
    var labels = completions.map((c) => c.label).toList();
    expect(labels, contains('override'));
    expect(labels, contains('deprecated'));
    expect(labels, contains('Deprecated(…)'));
  }

  Future<void> test_closure() async {
    content = '''
void f({void Function(int a, String b) closure}) {}

void g() {
  f(closure: ^);
}
''';

    var expectedContent = '''
void f({void Function(int a, String b) closure}) {}

void g() {
  f(closure: (a, b) => ^,);
}
''';

    await verifyCompletions(
      mainFileUri,
      content,
      expectCompletions: ['(a, b) =>', '(a, b) {}'],
      applyEditsFor: '(a, b) =>',
      expectedContent: expectedContent,
    );
  }

  Future<void> test_closure_requiredNamed() async {
    content = '''
void f({void Function({int a, required String b}) closure}) {}

void g() {
  f(closure: ^);
}
''';

    var expectedContent = '''
void f({void Function({int a, required String b}) closure}) {}

void g() {
  f(closure: ({a, required b}) => ^,);
}
''';

    await verifyCompletions(
      mainFileUri,
      content,
      // Display text does not contain 'required' because it makes the
      // completion much longer, we just include it in the completion text.
      expectCompletions: ['({a, b}) =>', '({a, b}) {}'],
      applyEditsFor: '({a, b}) =>',
      expectedContent: expectedContent,
    );
  }

  Future<void> test_closure_unnamedParameters() async {
    var content = '''
void f(void Function(int) x) {
  f(^);
}
''';

    var expectedContent = '''
void f(void Function(int) x) {
  f((p0) => ^,);
}
''';

    await verifyCompletions(
      mainFileUri,
      content,
      expectCompletions: ['(p0) =>', '(p0) {}'],
      applyEditsFor: '(p0) =>',
      expectedContent: expectedContent,
    );
  }

  Future<void> test_color_material() async {
    content = '''
import 'package:flutter/material.dart';
var a = Colors.re^
''';

    await expectColorCompletion(content, 'red', '#F44336');
  }

  Future<void> test_color_materialAccent() async {
    content = '''
import 'package:flutter/material.dart';
var a = Colors.redAcce^
''';

    await expectColorCompletion(content, 'redAccent', '#FF5252');
  }

  Future<void> test_comment() async {
    content = '''
// foo ^
void f() {}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res, isEmpty);
  }

  Future<void> test_comment_endOfFile_withNewline() async {
    // Checks for a previous bug where invoking completion inside a comment
    // at the end of a file would return results.
    content = '''
    // foo ^
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res, isEmpty);
  }

  Future<void> test_comment_endOfFile_withoutNewline() async {
    // Checks for a previous bug where invoking completion inside a comment
    // at the very end of a file with no trailing newline would return results.
    content = '// foo ^';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res, isEmpty);
  }

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

    Registration registration(Method method) =>
        registrationForDart(registrations, method);

    // By default, there should be no commit characters.
    var reg = registration(Method.textDocument_completion);
    var options = CompletionRegistrationOptions.fromJson(
      reg.registerOptions as Map<String, Object?>,
    );
    expect(options.allCommitCharacters, isNull);

    // When we change config, we should get a re-registration (unregister then
    // register) for completion which now includes the commit characters.
    await monitorDynamicReregistration(
      registrations,
      () => updateConfig({'previewCommitCharacters': true}),
    );
    reg = registration(Method.textDocument_completion);
    options = CompletionRegistrationOptions.fromJson(
      reg.registerOptions as Map<String, Object?>,
    );
    expect(options.allCommitCharacters, equals(dartCompletionCommitCharacters));
  }

  Future<void> test_completeFunctionCalls_constructor() async {
    await checkCompleteFunctionCallInsertText(
      '''
class Aaaaa {
  Aaaaa(int a);
}
void f(int aaa) {
  var a = new [!Aaa^!]
}
''',
      'Aaaaa(…)',
      insertTextFormat: InsertTextFormat.Snippet,
      editText: r'Aaaaa(${0:a})',
    );
  }

  Future<void> test_completeFunctionCalls_escapesDollarArgs() async {
    await checkCompleteFunctionCallInsertText(
      r'''
        int myFunction(String a$a, int b, {String c}) {
          var a = [!myFu^!]
        }
''',
      'myFunction(…)',
      insertTextFormat: InsertTextFormat.Snippet,
      // The dollar should have been escaped.
      editText: r'myFunction(${1:a\$a}, ${2:b})',
    );
  }

  Future<void> test_completeFunctionCalls_escapesDollarName() async {
    await checkCompleteFunctionCallInsertText(
      r'''
        int myFunc$tion(String a, int b, {String c}) {
          var a = [!myFu^!]
        }
''',
      r'myFunc$tion(…)',
      insertTextFormat: InsertTextFormat.Snippet,
      // The dollar should have been escaped.
      editText: r'myFunc\$tion(${1:a}, ${2:b})',
    );
  }

  Future<void> test_completeFunctionCalls_existingArgList_constructor() async {
    await checkCompleteFunctionCallInsertText(
      '''
class Aaaaa {
  Aaaaa(int a);
}
void f(int aaa) {
  var a = new [!Aaa^!]()
}
''',
      'Aaaaa(…)',
      editText: 'Aaaaa',
    );
  }

  Future<void> test_completeFunctionCalls_existingArgList_expression() async {
    await checkCompleteFunctionCallInsertText(
      '''
        int myFunction(String a, int b, {String c}) {
          var a = [!myFu^!]()
        }
''',
      'myFunction(…)',
      editText: 'myFunction',
    );
  }

  Future<void>
  test_completeFunctionCalls_existingArgList_member_noPrefix() async {
    // https://github.com/Dart-Code/Dart-Code/issues/3672
    await checkCompleteFunctionCallInsertText(
      '''
class Aaaaa {
  static foo(int a) {}
}
void f() {
  Aaaaa.[!^!]()
}
''',
      'foo(…)',
      editText: 'foo',
    );
  }

  Future<void>
  test_completeFunctionCalls_existingArgList_namedConstructor() async {
    await checkCompleteFunctionCallInsertText(
      '''
class Aaaaa {
  Aaaaa.foo(int a);
}
void f() {
  var a = new Aaaaa.[!foo^!]()
}
''',
      'foo(…)',
      editText: 'foo',
    );
  }

  Future<void> test_completeFunctionCalls_existingArgList_statement() async {
    await checkCompleteFunctionCallInsertText(
      '''
void f(int a) {
  [!f^!]()
}
''',
      'f(…)',
      editText: 'f',
    );
  }

  Future<void>
  test_completeFunctionCalls_existingArgList_suggestionSets() async {
    await checkCompleteFunctionCallInsertText(
      '''
void f(int a) {
  [!pri^!]()
}
''',
      'print(…)',
      editText: 'print',
    );
  }

  Future<void> test_completeFunctionCalls_existingPartialArgList() async {
    await checkCompleteFunctionCallInsertText(
      '''
class Aaaaa {
  Aaaaa(int a);
}
void f(int aaa) {
  var a = new [!Aaa^!](
}
''',
      'Aaaaa(…)',
      editText: 'Aaaaa',
    );
  }

  Future<void> test_completeFunctionCalls_expression() async {
    await checkCompleteFunctionCallInsertText(
      '''
int myFunction(String a, int b, {String c}) {
  var a = [!myFu^!]
}
''',
      'myFunction(…)',
      insertTextFormat: InsertTextFormat.Snippet,
      editText: r'myFunction(${1:a}, ${2:b})',
    );
  }

  Future<void> test_completeFunctionCalls_flutterSetState() async {
    // Flutter's setState method has special handling inside SuggestionBuilder
    // that already adds in a selection (which overlaps with completeFunctionCalls).
    // Ensure we don't end up with two sets of parens/placeholders in this case.
    content = '''
import 'package:flutter/material.dart';

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    [!setSt^!]
    return const Placeholder();
  }
}
''';

    setCompletionItemSnippetSupport();
    await provideConfig(initialize, {'completeFunctionCalls': true});

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label.startsWith('setState('));

    // Usually the label would be "setState(…)" but here it's slightly different
    // to indicate a full statement is being inserted.
    expect(item.label, equals('setState(() {});'));

    // Ensure the snippet comes through in the expected format with the expected
    // placeholders.
    expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
    expect(item.insertText, isNull);
    var textEdit = toTextEdit(item.textEdit!);
    expect(textEdit.newText, 'setState(() {$eol      \$0$eol    });');
    expect(textEdit.range, equals(code.range.range));
  }

  Future<void> test_completeFunctionCalls_namedConstructor() async {
    await checkCompleteFunctionCallInsertText(
      '''
class Aaaaa {
  Aaaaa.foo(int a);
}
void f() {
  var a = new Aaaaa.[!foo^!]
}
''',
      'foo(…)',
      insertTextFormat: InsertTextFormat.Snippet,
      editText: r'foo(${0:a})',
    );
  }

  Future<void> test_completeFunctionCalls_noParameters() async {
    content = '''
void myFunction() {}

void f() {
  [!myFu^!]
}
''';

    await checkCompleteFunctionCallInsertText(
      content,
      'myFunction()',
      editText: 'myFunction()',
      insertTextFormat: InsertTextFormat.Snippet,
    );
  }

  Future<void> test_completeFunctionCalls_optionalParameters() async {
    content = '''
void myFunction({int a}) {}

void f() {
  [!myFu^!]
}
''';

    await checkCompleteFunctionCallInsertText(
      content,
      'myFunction(…)',
      // With optional params, there should still be parens/tab stop inside.
      editText: r'myFunction($0)',
      insertTextFormat: InsertTextFormat.Snippet,
    );
  }

  Future<void> test_completeFunctionCalls_requiredNamed() async {
    content = '''
void myFunction(String a, int b, {required String c, String d = ''}) {}

void f() {
  [!myFu^!]
}
''';

    setCompletionItemSnippetSupport();
    await provideConfig(initialize, {'completeFunctionCalls': true});

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label == 'myFunction(…)');
    // Ensure the snippet comes through in the expected format with the expected
    // placeholders.
    expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
    expect(item.insertText, isNull);
    var textEdit = toTextEdit(item.textEdit!);
    expect(textEdit.newText, r'myFunction(${1:a}, ${2:b}, c: ${3:c})');
    expect(textEdit.range, equals(code.range.range));
  }

  Future<void> test_completeFunctionCalls_requiredNamed_suggestionSet() async {
    var otherFile = join(projectFolderPath, 'lib', 'other.dart');
    newFile(
      otherFile,
      "void myFunction(String a, int b, {required String c, String d = ''}) {}",
    );
    content = '''
void f() {
  [!myFu^!]
}
''';

    setCompletionItemSnippetSupport();
    await provideConfig(initialize, {'completeFunctionCalls': true});

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label == 'myFunction(…)');
    // Ensure the snippet comes through in the expected format with the expected
    // placeholders.
    expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
    expect(item.insertText, isNull);
    expect(item.textEdit, isNotNull);
    var originalTextEdit = item.textEdit;

    // Ensure the item can be resolved and retains the correct textEdit (since
    // textEdit may be recomputed during resolve).
    var resolved = await resolveCompletion(item);
    expect(resolved.insertText, isNull);
    expect(resolved.textEdit, originalTextEdit);
    var textEdit = toTextEdit(resolved.textEdit!);
    expect(textEdit.newText, r'myFunction(${1:a}, ${2:b}, c: ${3:c})');
    expect(textEdit.range, equals(code.range.range));
  }

  Future<void>
  test_completeFunctionCalls_resolve_producesCorrectEditWithoutInsertText() async {
    // Ensure our `resolve` call does not rely on the presence of `insertText`
    // to compute the correct edits. This is something we did incorrectly in the
    // past and broke with
    // https://github.com/dart-lang/sdk/commit/40e25ebad0bd008615b1c1d8021cb27839f00dcd
    // because the way these are combined in the VS Code LSP client means we are
    // not provided both `insertText` and `textEdit` back in the resolve call.
    //
    // Now, we never supply `insertText` and always use `textEdit`.
    content = '''
final a = Stri^
''';

    /// Helper to verify a completion is as expected.
    void expectCorrectCompletion(CompletionItem item) {
      // Ensure this completion looks as we'd expect.
      expect(item.label, 'String.fromCharCode(…)');
      expect(item.insertText, isNull);
      expect(
        item.textEdit!.map((edit) => edit.newText, (edit) => edit.newText),
        r'String.fromCharCode(${0:charCode})',
      );
    }

    setCompletionItemSnippetSupport();
    await provideConfig(initialize, {'completeFunctionCalls': true});

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    var completion = res.singleWhere(
      (c) => c.label == 'String.fromCharCode(…)',
    );
    expectCorrectCompletion(completion);

    var resolved = await resolveCompletion(completion);
    expectCorrectCompletion(resolved);
  }

  Future<void> test_completeFunctionCalls_show() async {
    content = '''
import 'dart:math' show mi^
''';

    setCompletionItemSnippetSupport();
    await provideConfig(initialize, {'completeFunctionCalls': true});

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label == 'min(…)');
    // The insert text should be a simple string with no parens/args and
    // no need for snippets.
    expect(item.insertTextFormat, isNull);
    expect(item.insertText, isNull);
    var textEdit = toTextEdit(item.textEdit!);
    expect(textEdit.newText, r'min');
  }

  Future<void> test_completeFunctionCalls_statement() async {
    await checkCompleteFunctionCallInsertText(
      '''
void f(int a) {
  [!f^!]
}
''',
      'f(…)',
      insertTextFormat: InsertTextFormat.Snippet,
      editText: r'f(${0:a})',
    );
  }

  Future<void> test_completeFunctionCalls_suggestionSets() async {
    await checkCompleteFunctionCallInsertText(
      '''
void f(int a) {
  [!pri^!]
}
''',
      'print(…)',
      insertTextFormat: InsertTextFormat.Snippet,
      editText: r'print(${0:object})',
    );
  }

  Future<void> test_completionKinds_default() async {
    newFile(join(projectFolderPath, 'file.dart'), '');
    newFolder(join(projectFolderPath, 'folder'));

    content = "import '^';";

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);

    var file = res.singleWhere((c) => c.label == 'file.dart');
    var folder = res.singleWhere((c) => c.label == 'folder/');
    var builtin = res.singleWhere((c) => c.label == 'dart:core');
    // Default capabilities include File + Module but not Folder.
    expect(file.kind, equals(CompletionItemKind.File));
    // We fall back to Module if Folder isn't supported.
    expect(folder.kind, equals(CompletionItemKind.Module));
    expect(builtin.kind, equals(CompletionItemKind.Module));
  }

  Future<void> test_completionKinds_imports() async {
    // Tell the server we support some specific CompletionItemKinds.
    setCompletionItemKinds([
      CompletionItemKind.File,
      CompletionItemKind.Folder,
      CompletionItemKind.Module,
    ]);

    content = "import '^';";

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);

    var file = res.singleWhere((c) => c.label == 'file.dart');
    var folder = res.singleWhere((c) => c.label == 'folder/');
    var builtin = res.singleWhere((c) => c.label == 'dart:core');
    expect(file.kind, equals(CompletionItemKind.File));
    expect(folder.kind, equals(CompletionItemKind.Folder));
    expect(builtin.kind, equals(CompletionItemKind.Module));
  }

  Future<void> test_completionKinds_supportedSubset() async {
    // Tell the server we only support the Field CompletionItemKind.
    setCompletionItemKinds([CompletionItemKind.Field]);

    content = '''
class MyClass {
  String abcdefghij;
}

void f() {
  MyClass a;
  a.abc^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var kinds = res.map((item) => item.kind).toList();

    // Ensure we only get nulls or Fields (the sample code contains Classes).
    expect(
      kinds,
      everyElement(anyOf(isNull, equals(CompletionItemKind.Field))),
    );
  }

  Future<void> test_completionTrigger_brace_block() async {
    // Brace should not trigger completion if a normal code block.
    content = r'''
main () {^}
''';
    await _checkResultsForTriggerCharacters(content, ['{'], isEmpty);
  }

  Future<void>
  test_completionTrigger_brace_interpolatedStringExpression() async {
    // Brace should trigger completion if at the start of an interpolated expression
    content = r'''
var a = '${^';
''';
    await _checkResultsForTriggerCharacters(content, [r'{'], isNotEmpty);
  }

  Future<void> test_completionTrigger_brace_rawString() async {
    // Brace should not trigger completion if in a raw string.
    content = r'''
var a = r'${^';
''';
    await _checkResultsForTriggerCharacters(content, [r'{'], isEmpty);
  }

  Future<void> test_completionTrigger_brace_string() async {
    // Brace should not trigger completion if not at the start of an interpolated
    // expression.
    content = r'''
var a = '{^';
''';
    await _checkResultsForTriggerCharacters(content, [r'{'], isEmpty);
  }

  Future<void> test_completionTrigger_colon_argument() async {
    // Colons should trigger completion after argument names.
    content = r'''
void f({int? a}) {
  f(a:^
}
''';
    await _checkResultsForTriggerCharacters(content, [r':'], isNotEmpty);
  }

  Future<void> test_completionTrigger_colon_case() async {
    // Colons should not trigger completion in a switch case.
    content = r'''
void f(int a) {
  switch (a) {
    case:^
  }
}
''';
    await _checkResultsForTriggerCharacters(content, [r':'], isEmpty);
  }

  Future<void> test_completionTrigger_colon_default() async {
    // Colons should not trigger completion in a switch case.
    content = r'''
void f(int a) {
  switch (a) {
    default:^
  }
}
''';
    await _checkResultsForTriggerCharacters(content, [r':'], isEmpty);
  }

  Future<void> test_completionTrigger_colon_import() async {
    // Colons should trigger completion after argument names.
    content = r'''
import 'package:^';
''';
    await _checkResultsForTriggerCharacters(content, [r':'], isNotEmpty);
  }

  Future<void> test_completionTrigger_quotes_endingString() async {
    // Completion triggered by a quote ending a string should not return results.
    content = "foo(''^);";
    await _checkResultsForTriggerCharacters(content, ["'", '"'], isEmpty);
  }

  Future<void> test_completionTrigger_quotes_startingImport() async {
    // Completion triggered by a quote for import should return results.
    content = "import '^'";
    await _checkResultsForTriggerCharacters(content, ["'", '"'], isNotEmpty);
  }

  Future<void> test_completionTrigger_quotes_startingString() async {
    // Completion triggered by a quote for normal string should not return results.
    content = "foo('^');";
    await _checkResultsForTriggerCharacters(content, ["'", '"'], isEmpty);
  }

  Future<void> test_completionTrigger_quotes_terminatingImport() async {
    // Completion triggered by a quote ending an import should not return results.
    content = "import ''^";
    await _checkResultsForTriggerCharacters(content, ["'", '"'], isEmpty);
  }

  Future<void> test_completionTrigger_slash_directivePath() async {
    // Slashes should trigger completion when typing in directive paths, eg.
    // after typing 'package:foo/' completion should give the next folder segments.
    content = r'''
    import 'package:test/^';
''';
    await _checkResultsForTriggerCharacters(content, [r'/'], isNotEmpty);
  }

  Future<void> test_completionTrigger_slash_divide() async {
    // Slashes should not trigger completion when typing in a normal expression.
    content = r'''
var a = 1 /^
''';
    await _checkResultsForTriggerCharacters(content, [r'/'], isEmpty);
  }

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

    var invalidTriggerKind = CompletionTriggerKind.fromJson(-1);
    var request = getCompletion(
      mainFileUri,
      Position(line: 0, character: 0),
      context: CompletionContext(
        triggerKind: invalidTriggerKind,
        triggerCharacter: 'A',
      ),
    );

    await expectLater(
      request,
      throwsA(isResponseError(ErrorCodes.InvalidParams)),
    );
  }

  Future<void> test_concurrentRequestsCancellation() async {
    // We expect a new completion request to cancel any in-flight request so
    // send multiple without awaiting, then check only the last one completes.
    var code = TestCode.empty;

    await initialize();
    await openFile(mainFileUri, code.code);
    var position = code.position.position;

    // Use a completer to force the requests to overlap without races.
    var completer = Completer<void>();
    CompletionHandler.delayAfterResolveForTests = completer.future;
    try {
      var responseFutures = [
        getCompletion(mainFileUri, position),
        getCompletion(mainFileUri, position),
        getCompletion(mainFileUri, position),
      ];

      var expectationFutures = [
        expectLater(
          responseFutures[0],
          throwsA(
            isResponseError(
              ErrorCodes.RequestCancelled,
              message: 'Another textDocument/completion request was started',
            ),
          ),
        ),
        expectLater(
          responseFutures[1],
          throwsA(
            isResponseError(
              ErrorCodes.RequestCancelled,
              message: 'Another textDocument/completion request was started',
            ),
          ),
        ),
        expectLater(responseFutures[2], completion(isNotEmpty)),
      ];

      // Ensure all requests started, then let them continue. This must be done
      // after the expectations are set up above, because otherwise if the
      // exceptions occur too quickly, they will be unhandled (whereas the
      // expectations attach error handlers to them).
      await pumpEventQueue(times: 50000);
      completer.complete();
      await Future.wait(expectationFutures);
    } finally {
      CompletionHandler.delayAfterResolveForTests = null;
    }
  }

  Future<void> test_dartDocPreference_full() async {
    await assertDocumentation(
      'full',
      includesSummary: true,
      includesFull: true,
    );
  }

  Future<void> test_dartDocPreference_none() async {
    await assertDocumentation(
      'none',
      includesSummary: false,
      includesFull: false,
    );
  }

  Future<void> test_dartDocPreference_summary() async {
    await assertDocumentation(
      'summary',
      includesSummary: true,
      includesFull: false,
    );
  }

  /// No preference should result in full docs.
  Future<void> test_dartDocPreference_unset() async {
    await assertDocumentation(null, includesSummary: true, includesFull: true);
  }

  Future<void> test_filterText_constructorParens() async {
    // Constructor parens should not be included in filterText.
    content = '''
class MyClass {}

void f() {
  MyClass a = new MyCla^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'MyClass()'), isTrue);
    var item = res.singleWhere((c) => c.label == 'MyClass()');

    // filterText is set explicitly because it's not the same as label.
    expect(item.filterText, 'MyClass');

    // The text in the edit should also not contain the parens.
    var textEdit = toTextEdit(item.textEdit!);
    expect(textEdit.newText, 'MyClass');
  }

  Future<void> test_filterText_override_getter() async {
    // Some completions (eg. overrides) have additional text that is not part
    // of the label. That text should _not_ appear in filterText as it will
    // affect the editors relevance ranking as the user types.
    // https://github.com/dart-lang/sdk/issues/45157
    content = '''
abstract class Person {
  String get name;
}

class Student extends Person {
  nam^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label == 'override name => …');
    // filterText is set explicitly because it's not the same as label.
    expect(item.filterText, 'name');
  }

  Future<void> test_filterText_override_method() async {
    // Some completions (eg. overrides) have additional text that is not part
    // of the label. That text should _not_ appear in filterText as it will
    // affect the editors relevance ranking as the user types.
    // https://github.com/dart-lang/sdk/issues/45157
    content = '''
abstract class Base {
  void myMethod() {};
}

class BaseImpl extends Base {
  myMet^
}
''';

    await initialize();
    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label == 'override myMethod() { … }');
    // filterText is set explicitly because it's not the same as label.
    expect(item.filterText, 'myMethod');
  }

  /// Check that narrowing a type from String? to String in a subclass includes
  /// the correct narrowed type in the `detail` field.
  ///
  /// https://github.com/Dart-Code/Dart-Code/issues/4499
  Future<void> test_getter_narrowedBySubclass() async {
    content = '''
void f(MyItem item) {
  item.na^
}

abstract class NullableName {
  String? get name;
}

abstract class NotNullableName implements NullableName {
  @override
  String get name;
}

abstract class MyItem implements NotNullableName, NullableName {}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var name = res.singleWhere((c) => c.label == 'name');
    expect(name.detail, equals('String'));
  }

  Future<void> test_gettersAndSetters() async {
    content = '''
class MyClass {
  String get justGetter => '';
  set justSetter(String value) {}
  String get getterAndSetter => '';
  set getterAndSetter(String value) {}
}

void f() {
  MyClass a;
  a.^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var getter = res.singleWhere((c) => c.label == 'justGetter');
    var setter = res.singleWhere((c) => c.label == 'justSetter');
    var both = res.singleWhere((c) => c.label == 'getterAndSetter');
    expect(getter.detail, equals('String'));
    expect(setter.detail, equals('String'));
    expect(both.detail, equals('String'));
    for (var item in [getter, setter, both]) {
      expect(item.kind, equals(CompletionItemKind.Property));
    }
  }

  Future<void> test_import() async {
    content = '''
import '^';
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'dart:async'), isTrue);
  }

  Future<void> test_import_configuration() async {
    content = '''
import 'dart:core' if (dart.library.io) '^';
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'dart:async'), isTrue);
  }

  Future<void> test_import_configuration_eof() async {
    content = '''
import 'dart:core' if (dart.library.io) '^
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'dart:async'), isTrue);
  }

  Future<void> test_import_configuration_partial() async {
    content = '''
import 'dart:core' if (dart.library.io) 'dart:^';
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'dart:async'), isTrue);
  }

  Future<void> test_import_eof() async {
    content = '''
import '^
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'dart:async'), isTrue);
  }

  Future<void> test_import_partial() async {
    content = '''
import 'dart:^';
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'dart:async'), isTrue);
  }

  Future<void> test_importedSymbol_libraryImported_hidingVariable() async {
    newFile(join(projectFolderPath, 'lib', 'import.dart'), '''
enum MyEnum {
  value1,
  value2,
}
void myFunction(MyEnum _) {}
var myVariable = 1;
''');

    content = '''
import 'import.dart' hide myVariable;

void main() {
  myFunction(MyE^);
}
''';

    var expectedContent = '''
import 'import.dart' hide myVariable;

void main() {
  myFunction(MyEnum.value1);
}
''';

    var completionLabel = 'MyEnum.value1';

    await _checkCompletionEdits(mainFileUri, completionLabel, expectedContent);
  }

  Future<void> test_importedSymbol_libraryImported_showingEnum() async {
    newFile(join(projectFolderPath, 'lib', 'import.dart'), '''
enum MyEnum {
  value1,
  value2,
}
''');

    content = '''
import 'import.dart' show MyEnum;

void main() {
  MyEnum _ = My^;
}
''';

    var expectedContent = '''
import 'import.dart' show MyEnum;

void main() {
  MyEnum _ = MyEnum.value1;
}
''';

    var completionLabel = 'MyEnum.value1';

    await _checkCompletionEdits(mainFileUri, completionLabel, expectedContent);
  }

  Future<void> test_insertReplaceRanges() async {
    setCompletionItemInsertReplaceSupport();

    content = '''
class MyClass {
  String abcdefghij;
}

void f() {
  MyClass a;
  a.abc^def
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'abcdefghij'), isTrue);
    var item = res.singleWhere((c) => c.label == 'abcdefghij');
    // When using the replacement range, we should get exactly the symbol
    // we expect.
    var replaced = applyTextEdits(code.code, [
      textEditForReplace(item.textEdit!),
    ]);
    expect(replaced, contains('a.abcdefghij$eol'));
    // When using the insert range, we should retain what was after the caret
    // ("def" in this case).
    var inserted = applyTextEdits(code.code, [
      textEditForInsert(item.textEdit!),
    ]);
    expect(inserted, contains('a.abcdefghijdef$eol'));
  }

  Future<void> test_insertTextMode_multiline() async {
    setCompletionItemInsertTextModeSupport();
    content = '''
import 'package:flutter/material.dart';

class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    [!setSt^!]
    return const Placeholder();
  }
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label.startsWith('setState'));

    // Multiline completions should always set insertTextMode.asIs.
    expect(item.insertText, isNull);
    var textEdit = toTextEdit(item.textEdit!);
    expect(textEdit.newText, contains(eol));
    expect(item.insertTextMode, equals(InsertTextMode.asIs));
  }

  Future<void> test_insertTextMode_singleLine() async {
    setCompletionItemInsertTextModeSupport();

    content = '''
void foo() {
  ^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label.startsWith('foo'));

    // Single line completions should never set insertTextMode.asIs to
    // avoid bloating payload size where it wouldn't matter.
    expect(item.insertText, isNull);
    var textEdit = toTextEdit(item.textEdit!);
    expect(textEdit.newText, isNot(contains('\n')));
    expect(item.insertTextMode, isNull);
  }

  Future<void> test_inside_lateFinal() async {
    await checkCompleteFunctionCallInsertText(
      '''
class MyClass {
  String get myGetter => '';
  late final myField = [!myG^!]
}
''',
      'myGetter',
      editText: 'myGetter',
    );
  }

  Future<void> test_inside_nonLateFinal() async {
    content = '''
class MyClass {
  String get myGetter => '';
  final myField = myG^
}
''';
    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res, isEmpty);
  }

  Future<void> test_insideString() async {
    content = '''
var a = "This is ^a test"
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res, isEmpty);
  }

  Future<void> test_isDeprecated_notSupported() async {
    content = '''
class MyClass {
  @deprecated
  String abcdefghij;
}

void f() {
  MyClass a;
  a.abc^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label == 'abcdefghij');
    expect(item.deprecated, isNull);
    // If the does not say it supports the deprecated flag, we should show
    // '(deprecated)' in the details.
    expect(item.detail!.toLowerCase(), contains('deprecated'));
  }

  Future<void> test_isDeprecated_supportedFlag() async {
    setCompletionItemDeprecatedFlagSupport();
    content = '''
class MyClass {
  @deprecated
  String abcdefghij;
}

void f() {
  MyClass a;
  a.abc^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label == 'abcdefghij');
    expect(item.deprecated, isTrue);
    // If the client says it supports the deprecated flag, we should not show
    // deprecated in the details.
    expect(item.detail, isNot(contains('deprecated')));
  }

  Future<void> test_isDeprecated_supportedTag() async {
    setCompletionItemTagSupport([CompletionItemTag.Deprecated]);

    content = '''
class MyClass {
  @deprecated
  String abcdefghij;
}

void f() {
  MyClass a;
  a.abc^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label == 'abcdefghij');
    expect(item.tags, contains(CompletionItemTag.Deprecated));
    // If the client says it supports the deprecated tag, we should not show
    // deprecated in the details.
    expect(item.detail, isNot(contains('deprecated')));
  }

  Future<void> test_isDeprecatedExtend_supportedFlag() async {
    setCompletionItemDeprecatedFlagSupport();
    content = '''
@Deprecated.extend()
class MyClass {}

void f() {
  MyCl^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label == 'MyClass');
    expect(item.deprecated, isNull);
    // If the client says it supports the deprecated flag, we should not show
    // deprecated in the details.
    expect(item.detail, isNot(contains('deprecated')));
  }

  Future<void> test_isDeprecatedExtend_supportedTag() async {
    setCompletionItemTagSupport([CompletionItemTag.Deprecated]);

    content = '''
@Deprecated.extend()
class MyClass {}

void f() {
  MyCl^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var item = res.singleWhere((c) => c.label == 'MyClass');
    expect(item.tags, isNot(contains(CompletionItemTag.Deprecated)));
    // If the client says it supports the deprecated tag, we should not show
    // deprecated in the details.
    expect(item.detail, isNot(contains('deprecated')));
  }

  Future<void> test_isIncomplete_falseIfAllIncluded() async {
    content = '''
import 'a.dart';
void f() {
  A a = A();
  a.^
}
''';

    // Create a class with fields aaa1 to aaa500 in the other file.
    newFile(
      join(projectFolderPath, 'lib', 'a.dart'),
      [
        'class A {',
        for (var i = 1; i <= 500; i++) 'String get aaa$i => "";',
        '}',
      ].join(eol),
    );

    await initialize();
    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletionList(mainFileUri, code.position.position);

    // Expect everything (hashCode etc. will take it over 500).
    expect(res.items, hasLength(greaterThanOrEqualTo(500)));
    expect(res.isIncomplete, isFalse);
  }

  Future<void> test_isIncomplete_trueIfNotAllIncluded() async {
    content = '''
import 'a.dart';
void f() {
  A a = A();
  a.^
}
''';

    // Create a class with fields aaa1 to aaa500 in the other file.
    newFile(
      join(projectFolderPath, 'lib', 'a.dart'),
      [
        'class A {',
        for (var i = 1; i <= 500; i++) '  String get aaa$i => "";',
        '  String get aaa => "";',
        '}',
      ].join(eol),
    );

    await provideConfig(initialize, {'maxCompletionItems': 200});
    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletionList(mainFileUri, code.position.position);

    // Should be capped at 200 and marked as incomplete.
    expect(res.items, hasLength(200));
    expect(res.isIncomplete, isTrue);
  }

  Future<void> test_itemDefaults_editRange() async {
    setCompletionItemInsertReplaceSupport();
    setCompletionListDefaults(['editRange']);

    content = '''
void myFunction() {
  [!myFunctio^!]
}
''';

    await initialize();
    await openFile(mainFileUri, code.code);
    var list = await getCompletionList(mainFileUri, code.position.position);
    var item = list.items.singleWhere((c) => c.label.startsWith('myFunction'));
    var defaultEditRange = list.itemDefaults!.editRange!.map(
      (insertReplace) => throw 'Expected Range, got CompletionItemEditRange',
      (range) => range,
    );

    // Range covers the ranged marked with [!braces!] in `content`.
    expect(defaultEditRange, code.range.range);

    // Item should use the default range.
    expectUsesDefaultEditRange(item, 'myFunction');
  }

  Future<void> test_itemDefaults_editRange_includesNonDefaultItem() async {
    setCompletionItemInsertReplaceSupport();
    setCompletionListDefaults(['editRange']);

    // In this code, we will get two completions with different edit ranges:
    //
    //   - 'b: ' will have a zero-width range because names don't replace args
    //   -  'a' will replace 'b'
    //
    // Therefore we expect 'a' to use the default range (and not have its own)
    // but 'b'` to have its own.
    //
    // Additionally, because the caret is before the identifier, we will have
    // separate default insert/replace ranges.
    content = '''
void f(String a, {String? b}) {
  f([!^b!]);
}
''';

    await initialize();
    await openFile(mainFileUri, code.code);
    var list = await getCompletionList(mainFileUri, code.position.position);
    var itemA = list.items.singleWhere((c) => c.label == 'a');
    var itemB = list.items.singleWhere((c) => c.label == 'b: ');

    // Default replace range should span `b`.
    var expectedRange = code.range.range;
    var defaultEditRange = list.itemDefaults!.editRange!.map(
      (insertReplace) => insertReplace,
      (range) => throw 'Expected Range, got CompletionItemEditRange',
    );
    expect(defaultEditRange.replace, equals(expectedRange));

    // Default insert range should be in front of `b`.
    expect(
      defaultEditRange.insert,
      Range(start: expectedRange.start, end: expectedRange.start),
    );

    // And item A should use that default.
    expectUsesDefaultEditRange(itemA, 'a');

    // Item B should have its own range, which is a single range for both
    // insert and replace that matches the insert range (in front of `b`) of
    // the default.
    var itemBTextEdit = toTextEdit(itemB.textEdit!);
    expect(itemBTextEdit.range, defaultEditRange.insert);
    expect(itemBTextEdit.newText, 'b: ');
  }

  Future<void> test_itemDefaults_textMode() async {
    setCompletionItemInsertTextModeSupport();
    setCompletionListDefaults(['insertTextMode']);

    // We only normally set InsertTextMode on multiline completions (where it
    // matters), so ensure there's a multiline completion in the results for
    // testing.
    content = '''
import 'package:flutter/material.dart';

class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    [!setSt^!]
    return const Placeholder();
  }
}
''';

    await initialize();
    await openFile(mainFileUri, code.code);
    var list = await getCompletionList(mainFileUri, code.position.position);
    var item = list.items.singleWhere((c) => c.label.startsWith('setState'));

    // Default should be set.
    expect(list.itemDefaults?.insertTextMode, InsertTextMode.asIs);
    // Item should not.
    expect(item.insertTextMode, isNull);
  }

  /// Exact matches should always be included when completion lists are
  /// truncated, even if they ranked poorly.
  Future<void> test_maxCompletionItems_doesNotExcludeExactMatches() async {
    content = '''
import 'a.dart';
void f() {
  var a = Item^
}
''';

    // Create classes `Item1` to `Item20` along with a field named `item`.
    // The classes will rank higher in the position above and push
    // the field out without an exception to include exact matches.
    newFile(
      join(projectFolderPath, 'lib', 'a.dart'),
      [
        'String item = "";',
        for (var i = 1; i <= 20; i++) 'class Item$i {}',
      ].join(eol),
    );

    await provideConfig(initialize, {'maxCompletionItems': 10});
    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletionList(mainFileUri, code.position.position);

    expect(res.items, hasLength(10));
    expect(res.isIncomplete, isTrue);

    // Ensure the 'Item' field is included.
    expect(res.items.map((item) => item.label), contains('item'));
  }

  /// Snippet completions should be kept when maxCompletionItems truncates
  /// because they are not ranked like other completions and might be
  /// truncated when they are exactly what the user wants.
  Future<void> test_maxCompletionItems_doesNotExcludeSnippets() async {
    content = '''
import 'a.dart';
void f() {
  fo^
}
''';

    // Create fields for1 to for20 in the other file.
    newFile(
      join(projectFolderPath, 'lib', 'a.dart'),
      [
        for (var i = 1; i <= 20; i++)
          'String for$i = '
              ';',
      ].join(eol),
    );

    setCompletionItemSnippetSupport();
    await provideConfig(initialize, {'maxCompletionItems': 10});
    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletionList(mainFileUri, code.position.position);

    // Should be capped at 10 and marked as incomplete.
    expect(res.items, hasLength(10));
    expect(res.isIncomplete, isTrue);

    // Ensure the 'for' snippet is included.
    expect(
      res.items
          .where((item) => item.kind == CompletionItemKind.Snippet)
          .map((item) => item.label)
          .contains('for'),
      isTrue,
    );
  }

  Future<void> test_namedArg_flutterChildren() async {
    content = '''
import 'package:flutter/widgets.dart';

final a = Flex(c^);
''';

    var expectedContent = '''
import 'package:flutter/widgets.dart';

final a = Flex(children: [^],);
''';

    await verifyCompletions(
      mainFileUri,
      content,
      expectCompletions: ['children: []'],
      applyEditsFor: 'children: []',
      expectedContent: expectedContent,
    );
  }

  Future<void> test_namedArg_flutterChildren_existingValue() async {
    // Flutter's widget classes have special handling that adds `[]` after the
    // children named arg, but this should not occur if there's already a value
    // for this named arg.
    content = '''
import 'package:flutter/widgets.dart';

final a = Flex(c^: []);
''';

    var expectedContent = '''
import 'package:flutter/widgets.dart';

final a = Flex(children: []);
''';

    await verifyCompletions(
      mainFileUri,
      content,
      expectCompletions: ['children'],
      applyEditsFor: 'children',
      expectedContent: expectedContent,
    );
  }

  Future<void> test_namedArg_insertReplaceRanges() async {
    /// Helper to check multiple completions in the same template file.
    Future<void> check(
      String code,
      String expectedLabel, {
      required String expectedReplace,
      required String expectedInsert,
    }) async {
      content =
          '''
class A { const A({int argOne, int argTwo, String argThree}); }
final varOne = '';
$code
void f() { }
''';
      var expectedReplaced =
          '''
class A { const A({int argOne, int argTwo, String argThree}); }
final varOne = '';
$expectedReplace
void f() { }
''';
      var expectedInserted =
          '''
class A { const A({int argOne, int argTwo, String argThree}); }
final varOne = '';
$expectedInsert
void f() { }
''';

      await verifyCompletions(
        mainFileUri,
        content,
        expectCompletions: [expectedLabel],
        applyEditsFor: expectedLabel,
        verifyInsertReplaceRanges: true,
        expectedContent: expectedReplaced,
        expectedContentIfInserting: expectedInserted,
      );
    }

    // When at the start of the identifier, it will be set as the replacement
    // range so we don't expect the ': ,'
    await check(
      '@A(^argOne: 1)',
      'argTwo',
      expectedReplace: '@A(argTwo: 1)',
      expectedInsert: '@A(argTwoargOne: 1)',
    );

    // When adding a name to an existing value, it should always insert.
    await check(
      '@A(^1)',
      'argOne: ',
      expectedReplace: '@A(argOne: 1)',
      expectedInsert: '@A(argOne: 1)',
    );

    // When adding a name to an existing variable, it should always insert.
    await check(
      '@A(argOne: 1, ^varOne)',
      'argTwo: ',
      expectedReplace: '@A(argOne: 1, argTwo: varOne)',
      expectedInsert: '@A(argOne: 1, argTwo: varOne)',
    );

    // // Inside the identifier also should be expected to replace.
    await check(
      '@A(arg^One: 1)',
      'argTwo',
      expectedReplace: '@A(argTwo: 1)',
      expectedInsert: '@A(argTwoOne: 1)',
    );

    // If there's a space, there's no replacement range so we should still get
    // the colon/comma since this is always an insert (and both operations will
    // produce the same text).
    await check(
      '@A(^ argOne: 1)',
      'argTwo: ',
      expectedReplace: '@A(argTwo: ^, argOne: 1)',
      expectedInsert: '@A(argTwo: ^, argOne: 1)',
    );

    // Partially typed names in front of values (that aren't considered part of
    // the same identifier) should also suggest name labels.
    await check(
      '''@A(argOne: 1, argTh^'Foo')''',
      'argThree: ',
      expectedReplace: '''@A(argOne: 1, argThree: 'Foo')''',
      expectedInsert: '''@A(argOne: 1, argThree: 'Foo')''',
    );
  }

  Future<void> test_namedArg_offsetBeforeCompletionTarget() async {
    // This test checks for a previous bug where the completion target was a
    // symbol far after the cursor offset (`aaaa` here) and caused the whole
    // identifier to be used as the `targetPrefix` which would filter out
    // other symbol.
    // https://github.com/Dart-Code/Dart-Code/issues/2672#issuecomment-666085575
    content = '''
void f() {
  myFunction(
    ^
    aaaa: '',
  );
}

void myFunction({String aaaa, String aaab, String aaac}) {}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'aaab: '), isTrue);
  }

  Future<void> test_namedArg_plainText() async {
    content = '''
class A { const A({int one}); }
@A(^)
void f() { }
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'one: '), isTrue);
    var item = res.singleWhere((c) => c.label == 'one: ');
    expect(
      item.insertTextFormat,
      anyOf(equals(InsertTextFormat.PlainText), isNull),
    );
    expect(item.insertText, isNull);
    var textEdit = toTextEdit(item.textEdit!);
    expect(textEdit.newText, item.label);
    var updated = applyTextEdits(code.code, [toTextEdit(item.textEdit!)]);
    expect(updated, contains('one: '));
  }

  Future<void> test_namedArg_snippetStringSelection_endOfString() async {
    content = '''
class A { const A({int one}); }
@A(^)
void f() { }
''';

    setCompletionItemSnippetSupport();
    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'one: '), isTrue);
    var item = res.singleWhere((c) => c.label == 'one: ');
    // As the selection is the end of the string, there's no need for a snippet
    // here. Since the insert text is also the same as the label, it does not
    // need to be provided.
    expect(item.insertTextFormat, isNull);
    expect(item.insertText, isNull);
    var textEdit = toTextEdit(item.textEdit!);
    expect(textEdit.newText, equals('one: '));
    expect(
      textEdit.range,
      equals(Range(start: code.position.position, end: code.position.position)),
    );
  }

  Future<void>
  test_namedArgTrailing_snippetStringSelection_insideString() async {
    content = '''
void f({int one, int two}) {
  f(
    ^
    two: 2,
  );
}
''';

    setCompletionItemSnippetSupport();
    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'one: '), isTrue);
    var item = res.singleWhere((c) => c.label == 'one: ');
    // Ensure the snippet comes through in the expected format with the expected
    // placeholder.
    expect(item.insertTextFormat, equals(InsertTextFormat.Snippet));
    expect(item.insertText, isNull);
    var textEdit = toTextEdit(item.textEdit!);
    expect(textEdit.newText, equals(r'one: $0,'));
    expect(
      textEdit.range,
      equals(Range(start: code.position.position, end: code.position.position)),
    );
  }

  Future<void> test_nonAnalyzedFile() async {
    var readmeFilePath = convertPath(join(projectFolderPath, 'README.md'));
    newFile(readmeFilePath, '');
    await initialize();

    var res = await getCompletion(
      pathContext.toUri(readmeFilePath),
      startOfDocPos,
    );
    expect(res, isEmpty);
  }

  Future<void> test_noOperators_propertyAccessExternal() async {
    content = '''
class A {
  int operator-() => 0;
  int operator-(int other) => 0;
}

void f() {
  A().^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var completions = await getCompletion(mainFileUri, code.position.position);
    var labels = completions.map((c) => c.label).toList();
    expect(labels, isNot(contains('==(…)')));
    expect(labels, isNot(contains('-(…)')));
    expect(labels, isNot(contains('unary-()')));
  }

  Future<void> test_noOperators_propertyAccessInternal() async {
    content = '''
class A {
  int operator-() => 0;
  int operator-(int other) => 0;

  void m() {
    this.^
  }
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var completions = await getCompletion(mainFileUri, code.position.position);
    var labels = completions.map((c) => c.label).toList();
    expect(labels, isNot(contains('==(…)')));
    expect(labels, isNot(contains('-(…)')));
    expect(labels, isNot(contains('unary-()')));
  }

  Future<void> test_noOperators_superPropertyAccess() async {
    content = '''
class A {
  int operator-() => 0;
  int operator-(int other) => 0;
}

class B extends A {
  void m() {
    super.^
  }
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var completions = await getCompletion(mainFileUri, code.position.position);
    var labels = completions.map((c) => c.label).toList();
    expect(labels, isNot(contains('==(…)')));
    expect(labels, isNot(contains('-(…)')));
    expect(labels, isNot(contains('unary-()')));
  }

  Future<void> test_nullableTypes() async {
    content = '''
String? foo(int? a, [int b = 1]) {}

void f() {
  fo^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    var completion = res.singleWhere((c) => c.label.startsWith('foo'));
    expect(completion.detail, '(int? a, [int b = 1]) → String?');
  }

  Future<void> test_operators_override() async {
    content = '''
class A {
  int operator-() => 0;
  int operator-(int other) => 0;
}

class B extends A {
  ^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var completions = await getCompletion(mainFileUri, code.position.position);
    var labels = completions.map((c) => c.label).toList();
    expect(labels, contains('override ==(Object other) { … }'));
    expect(labels, contains('override -(int other) { … }'));
    expect(labels, contains('override -() { … }'));
  }

  Future<void> test_override() async {
    setCompletionItemSnippetSupport();
    content = '''
abstract class Base {
  String get name;
}

class Derived extends Base {
  ^
}
''';

    var expectedContent = r'''
abstract class Base {
  String get name;
}

class Derived extends Base {
  @override
  // TODO: implement name
  String get name => ${0:throw UnimplementedError()};
}
''';

    var completionLabel = 'override name => …';

    await _checkCompletionEdits(mainFileUri, completionLabel, expectedContent);
  }

  Future<void> test_plainText() async {
    content = '''
class MyClass {
  String abcdefghij;
}

void f() {
  MyClass a;
  a.abc^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'abcdefghij'), isTrue);
    var item = res.singleWhere((c) => c.label == 'abcdefghij');
    expect(
      item.insertTextFormat,
      anyOf(equals(InsertTextFormat.PlainText), isNull),
    );
    expect(item.insertText, anyOf(equals('abcdefghij'), isNull));
    var updated = applyTextEdits(code.code, [toTextEdit(item.textEdit!)]);
    expect(updated, contains('a.abcdefghij'));
  }

  Future<void> test_prefixed_enumMember() async {
    content = '''
import 'main.dart' as self;

enum MyEnum {
  one,
  two,
}

void main() {
  final x = self.MyEnum.^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'one'), isTrue);
  }

  Future<void> test_prefixFilter_endOfSymbol() async {
    content = '''
class UniqueNamedClassForLspOne {}
class UniqueNamedClassForLspTwo {}
class UniqueNamedClassForLspThree {}

void f() {
  // Should match only Two and Three
  UniqueNamedClassForLspT^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'UniqueNamedClassForLspOne'), isFalse);
    expect(res.any((c) => c.label == 'UniqueNamedClassForLspTwo'), isTrue);
    expect(res.any((c) => c.label == 'UniqueNamedClassForLspThree'), isTrue);
  }

  Future<void> test_prefixFilter_midSymbol() async {
    content = '''
class UniqueNamedClassForLspOne {}
class UniqueNamedClassForLspTwo {}
class UniqueNamedClassForLspThree {}

void f() {
  // Should match only Two and Three
  UniqueNamedClassForLspT^hree
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'UniqueNamedClassForLspOne'), isFalse);
    expect(res.any((c) => c.label == 'UniqueNamedClassForLspTwo'), isTrue);
    expect(res.any((c) => c.label == 'UniqueNamedClassForLspThree'), isTrue);
  }

  Future<void> test_prefixFilter_startOfSymbol() async {
    content = '''
class UniqueNamedClassForLspOne {}
class UniqueNamedClassForLspTwo {}
class UniqueNamedClassForLspThree {}

void f() {
  // Should match all three
  ^UniqueNamedClassForLspT
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'UniqueNamedClassForLspOne'), isTrue);
    expect(res.any((c) => c.label == 'UniqueNamedClassForLspTwo'), isTrue);
    expect(res.any((c) => c.label == 'UniqueNamedClassForLspThree'), isTrue);
  }

  Future<void> test_setters() async {
    content = '''
class MyClass {
  set stringSetter(String a) {}
  set noArgSetter() {}
  set multiArgSetter(a, b) {}
  set functionSetter(String Function(int a, int b) foo) {}
}

void f() {
  MyClass a;
  a.^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var setters = res
        .where((c) => c.label.endsWith('Setter'))
        .map((c) => c.detail != null ? '${c.label} (${c.detail})' : c.label)
        .toList();
    expect(setters, [
      'stringSetter (String)',
      'noArgSetter (dynamic)',
      'multiArgSetter (dynamic)',
      // Because of how we extract the type name, we don't currently support
      // this.
      'functionSetter',
    ]);
  }

  Future<void> test_sort_sortsByRelevance() async {
    content = '''
class UniquePrefixABC {}
class UniquePrefixAaBbCc {}

final a = UniquePrefixab^
''';

    await verifyCompletions(
      mainFileUri,
      content,
      expectCompletions: [
        // Constructors should all come before the class names, as they have
        // higher relevance in this position.
        'UniquePrefixABC()',
        'UniquePrefixAaBbCc()',
        'UniquePrefixABC',
        'UniquePrefixAaBbCc',
      ],
    );
  }

  Future<void> test_sort_truncatesByFuzzyScore() async {
    content = '''
class UniquePrefixABC {}
class UniquePrefixAaBbCc {}

final a = UniquePrefixab^
''';

    // Enable truncation after 2 items so we can verify which
    // items were dropped.
    await provideConfig(initialize, {'maxCompletionItems': 2});
    await verifyCompletions(
      mainFileUri,
      content,
      expectNoAdditionalItems: true,
      expectCompletions: [
        // Although constructors are more relevant, when truncating we will use
        // fuzzy score, so the closer matches are kept instead and we'll get
        // constructor+class from the closer match.
        'UniquePrefixABC()',
        'UniquePrefixABC',
      ],
    );
  }

  Future<void> test_unimportedSymbols() async {
    newFile(join(projectFolderPath, 'other_file.dart'), '''
/// This class is in another file.
class InOtherFile {}
''');

    content = '''
void f() {
  InOtherF^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    // Find the completion for the class in the other file.
    var completion = res.singleWhere((c) => c.label == 'InOtherFile');
    expect(completion, isNotNull);
    expect(completion.textEdit, isNotNull);
    var originalTextEdit = completion.textEdit;

    // Expect no docs, this is added during resolve.
    expectDocumentation(completion, isNull);

    // Resolve the completion item (via server) to get any additional edits.
    // This is LSP's equiv of getSuggestionDetails() and is invoked by LSP
    // clients to populate additional info (in our case, any additional edits
    // for inserting the import).
    var resolved = await resolveCompletion(completion);
    expect(resolved, isNotNull);

    // Ensure the detail field was update to show this will auto-import.
    expect(
      resolved.detail,
      startsWith("Auto import from '../other_file.dart'"),
    );

    // Ensure the doc comment was added.
    expectDocumentation(resolved, equals('This class is in another file.'));

    // Ensure the edit did not change.
    expect(resolved.textEdit, originalTextEdit);

    // There should be no command for this item because it doesn't need imports
    // in other files. Same-file completions are in additionalEdits.
    expect(resolved.command, isNull);

    // Apply both the main completion edit and the additionalTextEdits atomically.
    var newContent = applyTextEdits(
      code.code,
      [
        toTextEdit(resolved.textEdit!),
      ].followedBy(resolved.additionalTextEdits!).toList(),
    );

    // Ensure both edits were made - the completion, and the inserted import.
    expect(
      newContent,
      equalsNormalized('''
import '../other_file.dart';

void f() {
  InOtherFile
}
'''),
    );
  }

  Future<void> test_unimportedSymbols_dartDocPreference_full() async {
    await assertResolvedDocumentation(
      'full',
      includesSummary: true,
      includesFull: true,
    );
  }

  Future<void> test_unimportedSymbols_dartDocPreference_none() async {
    await assertResolvedDocumentation(
      'none',
      includesSummary: false,
      includesFull: false,
    );
  }

  Future<void> test_unimportedSymbols_dartDocPreference_summary() async {
    await assertResolvedDocumentation(
      'summary',
      includesSummary: true,
      includesFull: false,
    );
  }

  /// No preference should result in full docs.
  Future<void> test_unimportedSymbols_dartDocPreference_unset() async {
    await assertResolvedDocumentation(
      null,
      includesSummary: true,
      includesFull: true,
    );
  }

  Future<void>
  test_unimportedSymbols_doesNotDuplicate_importedViaMultipleLibraries() async {
    // An item that's already imported through multiple libraries that
    // export it should not result in multiple entries.
    newFile(join(projectFolderPath, 'lib/source_file.dart'), '''
class MyExportedClass {}
''');
    newFile(join(projectFolderPath, 'lib/reexport1.dart'), '''
export 'source_file.dart';
''');
    newFile(join(projectFolderPath, 'lib/reexport2.dart'), '''
export 'source_file.dart';
''');

    content = '''
import 'reexport1.dart';
import 'reexport2.dart';

void f() {
  MyExported^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);
    var completions = res.where((c) => c.label == 'MyExportedClass').toList();
    expect(completions, hasLength(1));
  }

  Future<void>
  test_unimportedSymbols_doesNotDuplicate_importedViaSingleLibrary() async {
    // An item that's already imported through a library that exports it
    // should not result in multiple entries.
    newFile(join(projectFolderPath, 'lib/source_file.dart'), '''
class MyExportedClass {}
''');
    newFile(join(projectFolderPath, 'lib/reexport1.dart'), '''
export 'source_file.dart';
''');
    newFile(join(projectFolderPath, 'lib/reexport2.dart'), '''
export 'source_file.dart';
''');

    content = '''
import 'reexport1.dart';

void f() {
  MyExported^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    var completions = res.where((c) => c.label == 'MyExportedClass').toList();
    expect(completions, hasLength(1));
  }

  Future<void> test_unimportedSymbols_doesNotFilterSymbolsWithSameName() async {
    // Classes here are not re-exports, so should not be filtered out.
    newFile(
      join(projectFolderPath, 'source_file1.dart'),
      'class MyDuplicatedClass {}',
    );
    newFile(
      join(projectFolderPath, 'source_file2.dart'),
      'class MyDuplicatedClass {}',
    );
    newFile(
      join(projectFolderPath, 'source_file3.dart'),
      'class MyDuplicatedClass {}',
    );

    content = '''
void f() {
  MyDuplicated^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    var completions = res.where((c) => c.label == 'MyDuplicatedClass').toList();
    expect(completions, hasLength(3));

    // Resolve the completions so we can get the auto-import text.
    var resolvedCompletions = await Future.wait(
      completions.map(resolveCompletion),
    );

    expectAutoImportCompletion(resolvedCompletions, '../source_file1.dart');
    expectAutoImportCompletion(resolvedCompletions, '../source_file2.dart');
    expectAutoImportCompletion(resolvedCompletions, '../source_file3.dart');
  }

  Future<void> test_unimportedSymbols_enumValues() async {
    // Enum values only show up in contexts with their types, so we need two
    // extra files - one with the Enum definition, and one with a function that
    // accepts the Enum type that is imported into the test files.
    newFile(join(projectFolderPath, 'lib', 'enum.dart'), '''
enum MyExportedEnum { One, Two }
''');
    newFile(join(projectFolderPath, 'lib', 'function_x.dart'), '''
import 'package:test/enum.dart';
void x(MyExportedEnum e) {}
''');

    content = '''
import 'package:test/function_x.dart';

void f() {
  x(MyExported^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    var enumCompletions = res
        .where((c) => c.label.startsWith('MyExportedEnum'))
        .toList();
    expect(
      enumCompletions.map((c) => c.label),
      unorderedEquals([
        'MyExportedEnum',
        'MyExportedEnum.One',
        'MyExportedEnum.Two',
      ]),
    );

    var completion = enumCompletions.singleWhere(
      (c) => c.label == 'MyExportedEnum.One',
    );

    // Resolve the completion item (via server) to get its edits. This is the
    // LSP's equiv of getSuggestionDetails() and is invoked by LSP clients to
    // populate additional info (in our case, the additional edits for inserting
    // the import).
    var resolved = await resolveCompletion(completion);
    expect(resolved, isNotNull);

    // Ensure the detail field was update to show this will auto-import.
    expect(
      resolved.detail,
      startsWith("Auto import from 'package:test/enum.dart'"),
    );

    // Ensure the edit was added on.
    expect(resolved.textEdit, isNotNull);

    // Apply both the main completion edit and the additionalTextEdits atomically.
    var newContent = applyTextEdits(
      code.code,
      [
        toTextEdit(resolved.textEdit!),
      ].followedBy(resolved.additionalTextEdits!).toList(),
    );

    // Ensure both edits were made - the completion, and the inserted import.
    expect(
      newContent,
      equalsNormalized('''
import 'package:test/enum.dart';
import 'package:test/function_x.dart';

void f() {
  x(MyExportedEnum.One
}
'''),
    );
  }

  Future<void> test_unimportedSymbols_enumValuesAlreadyImported() async {
    newFile(join(projectFolderPath, 'lib', 'enum.dart'), '''
enum MyExportedEnum { One, Two }
''');
    newFile(join(projectFolderPath, 'lib', 'reexport1.dart'), '''
import 'enum.dart';
export 'enum.dart';
void x(MyExportedEnum e) {}
''');
    newFile(join(projectFolderPath, 'lib', 'reexport2.dart'), '''
export 'enum.dart';
''');

    content = '''
import 'reexport1.dart';

void f() {
  x(MyExported^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    var completions = res
        .where((c) => c.label == 'MyExportedEnum.One')
        .toList();
    expect(completions, hasLength(1));
    var resolved = await resolveCompletion(completions.first);
    // It should not include auto-import text since it's already imported.
    expect(resolved.detail, isNot(contains('Auto import from')));
  }

  /// Verify extensions can be auto-imported if not already in-scope.
  Future<void> test_unimportedSymbols_extension() async {
    // Define extensions in 'extensions.dart'.
    newFile(join(projectFolderPath, 'lib', 'extensions.dart'), '''
extension StringExtensions on String {
  String get empty => '';
}
''');

    // Also import the extensions into an unrelated file to ensure this doesn't
    // cause extra suggestions (https://github.com/dart-lang/sdk/issues/56320).
    newFile(
      join(projectFolderPath, 'lib', 'other.dart'),
      'import "extensions.dart";',
    );

    content = '''
void f(String a) {
  a.empt^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    // Expect only a single entry for the 'empty' extension member.
    var completions = res.where((c) => c.label == 'empty');
    expect(completions, hasLength(1));

    // Expect it to auto-import from 'extensions.dart'.
    var resolved = await resolveCompletion(completions.single);
    expect(
      resolved.detail,
      startsWith("Auto import from 'package:test/extensions.dart'"),
    );

    // Verify the edits.
    var newContent = applyTextEdits(
      code.code,
      [
        toTextEdit(resolved.textEdit!),
      ].followedBy(resolved.additionalTextEdits!).toList(),
    );
    expect(
      newContent,
      equalsNormalized('''
import 'package:test/extensions.dart';

void f(String a) {
  a.empty
}
'''),
    );
  }

  Future<void> test_unimportedSymbols_filtersOutAlreadyImportedSymbols() async {
    newFile(join(projectFolderPath, 'lib', 'source_file.dart'), '''
class MyExportedClass {}
''');
    newFile(join(projectFolderPath, 'lib', 'reexport1.dart'), '''
export 'source_file.dart';
''');
    newFile(join(projectFolderPath, 'lib', 'reexport2.dart'), '''
export 'source_file.dart';
''');

    content = '''
import 'reexport1.dart';

void f() {
  MyExported^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    var completions = res.where((c) => c.label == 'MyExportedClass').toList();
    expect(completions, hasLength(1));
    var resolved = await resolveCompletion(completions.first);
    // It should not include auto-import text since it's already imported.
    expect(resolved.detail, isNull);
  }

  Future<void> test_unimportedSymbols_importsPackageUri() async {
    newFile(
      join(projectFolderPath, 'lib', 'my_class.dart'),
      'class MyClass {}',
    );

    content = '''
void f() {
  MyClas^
}
''';

    var expectedContent = '''
import 'package:test/my_class.dart';

void f() {
  MyClass
}
''';

    var completionLabel = 'MyClass';

    await _checkCompletionEdits(mainFileUri, completionLabel, expectedContent);
  }

  Future<void>
  test_unimportedSymbols_importsPackageUri_extensionMember() async {
    newFile(join(projectFolderPath, 'lib', 'my_extension.dart'), '''
extension MyExtension on String {
  void myExtensionMethod() {}
}
''');

    content = '''
void f() {
  ''.myExtensionMet^
}
''';

    var expectedContent = '''
import 'package:test/my_extension.dart';

void f() {
  ''.myExtensionMethod
}
''';

    var completionLabel = 'myExtensionMethod()';
    await _checkCompletionEdits(mainFileUri, completionLabel, expectedContent);
  }

  Future<void>
  test_unimportedSymbols_includesReexportedSymbolsForEachFile() async {
    newFile(join(projectFolderPath, 'source_file.dart'), '''
class MyExportedClass {}
''');
    newFile(join(projectFolderPath, 'reexport1.dart'), '''
export 'source_file.dart';
''');
    newFile(join(projectFolderPath, 'reexport2.dart'), '''
export 'source_file.dart';
''');

    content = '''
void f() {
  MyExported^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    var completions = res.where((c) => c.label == 'MyExportedClass').toList();
    expect(completions, hasLength(3));

    // Resolve the completions so we can get the auto-import text.
    var resolvedCompletions = await Future.wait(
      completions.map(resolveCompletion),
    );

    expectAutoImportCompletion(resolvedCompletions, '../source_file.dart');
    expectAutoImportCompletion(resolvedCompletions, '../reexport1.dart');
    expectAutoImportCompletion(resolvedCompletions, '../reexport2.dart');
  }

  Future<void> test_unimportedSymbols_insertReplaceRanges() async {
    setCompletionItemInsertReplaceSupport();

    newFile(join(projectFolderPath, 'other_file.dart'), '''
/// This class is in another file.
class InOtherFile {}
''');

    content = '''
void f() {
  InOtherF^il
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    // Find the completion for the class in the other file.
    var completion = res.singleWhere((c) => c.label == 'InOtherFile');
    expect(completion, isNotNull);
    expect(completion.textEdit, isNotNull);
    var originalTextEdit = completion.textEdit;

    // Expect no docs, this is added during resolve.
    expectDocumentation(completion, isNull);

    // Resolve the completion item (via server) to get any additional edits.
    // This is LSP's equiv of getSuggestionDetails() and is invoked by LSP
    // clients to populate additional info (in our case, any additional edits
    // for inserting the import).
    var resolved = await resolveCompletion(completion);
    expect(resolved, isNotNull);

    // Ensure the detail field was update to show this will auto-import.
    expect(
      resolved.detail,
      startsWith("Auto import from '../other_file.dart'"),
    );

    // Ensure the doc comment was added.
    expectDocumentation(resolved, equals('This class is in another file.'));

    // Ensure the edit did not change.
    expect(resolved.textEdit, originalTextEdit);

    // There should be no command for this item because it doesn't need imports
    // in other files. Same-file completions are in additionalEdits.
    expect(resolved.command, isNull);

    // Apply both the main completion edit and the additionalTextEdits atomically
    // then check the contents.

    var newContentReplaceMode = applyTextEdits(
      code.code,
      [
        textEditForReplace(resolved.textEdit!),
      ].followedBy(resolved.additionalTextEdits!).toList(),
    );
    var newContentInsertMode = applyTextEdits(
      code.code,
      [
        textEditForInsert(resolved.textEdit!),
      ].followedBy(resolved.additionalTextEdits!).toList(),
    );

    // Ensure both edits were made - the completion, and the inserted import.
    expect(
      newContentReplaceMode,
      equalsNormalized('''
import '../other_file.dart';

void f() {
  InOtherFile
}
'''),
    );
    // In insert mode, we'd have the trailing "il" still after the caret.
    expect(
      newContentInsertMode,
      equalsNormalized('''
import '../other_file.dart';

void f() {
  InOtherFileil
}
'''),
    );
  }

  Future<void> test_unimportedSymbols_insertsIntoPartFiles() async {
    // File we'll be adding an import for.
    newFile(join(projectFolderPath, 'other_file.dart'), ''''
class InOtherFile {}
''');

    // File that will have the import added.
    var parentContent = '''
part 'main.dart';
''';
    newFile(join(projectFolderPath, 'lib', 'parent.dart'), parentContent);

    // File that we're invoking completion in.
    content = '''
part of 'parent.dart';
void f() {
  InOtherF^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    var completion = res.singleWhere((c) => c.label == 'InOtherFile');
    expect(completion, isNotNull);

    // Resolve the completion item to get its edits.
    var resolved = await resolveCompletion(completion);
    expect(resolved, isNotNull);
    // Ensure it has a command, since it will need to make edits in other files
    // and that's done by telling the server to send a workspace/applyEdit. LSP
    // doesn't currently support these other-file edits in the completion.
    // See https://github.com/microsoft/language-server-protocol/issues/749
    expect(resolved.command, isNotNull);

    // Apply all current-document edits.
    var newContent = applyTextEdits(
      code.code,
      [
        toTextEdit(resolved.textEdit!),
      ].followedBy(resolved.additionalTextEdits!).toList(),
    );
    expect(
      newContent,
      equalsNormalized('''
part of 'parent.dart';
void f() {
  InOtherFile
}
'''),
    );

    await verifyCommandEdits(resolved.command!, '''
>>>>>>>>>> lib/parent.dart
import '../other_file.dart';

part 'main.dart';
''');
  }

  Future<void>
  test_unimportedSymbols_isIncompleteNotSetIfBudgetNotExhausted() async {
    content = '''
void f() {
  InOtherF^
}
''';

    await initialize(
      initializationOptions: {
        ...?defaultInitializationOptions,
        // Set budget high to ensure it completes.
        'completionBudgetMilliseconds': 100000,
      },
    );
    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletionList(mainFileUri, code.position.position);

    // Ensure we flagged that we returned everything.
    expect(res.isIncomplete, isFalse);
  }

  Future<void> test_unimportedSymbols_isIncompleteSetIfBudgetExhausted() async {
    newFile(
      join(projectFolderPath, 'lib', 'other_file.dart'),
      'class InOtherFile {}',
    );

    content = '''
void f() {
  InOtherF^
}
''';

    await initialize(
      initializationOptions: {
        ...?defaultInitializationOptions,
        // Set budget low to ensure we don't complete.
        'completionBudgetMilliseconds': 0,
      },
    );
    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletionList(mainFileUri, code.position.position);

    // Ensure we flagged that we did not return everything.
    expect(res.items, hasLength(0));
    expect(res.isIncomplete, isTrue);
  }

  Future<void> test_unimportedSymbols_libraryImported_hidingMultiple() async {
    newFile(join(projectFolderPath, 'lib', 'my_classes.dart'), '''
class MyClass1 {}
class MyClass2 {}
class MyClass3 {}
''');

    content = '''
import 'package:test/my_classes.dart' hide MyClass1, MyClass2;
void f() {
  MyClas^
}
''';

    var expectedContent = '''
import 'package:test/my_classes.dart' hide MyClass2;
void f() {
  MyClass1
}
''';

    var completionLabel = 'MyClass1';

    await _checkCompletionEdits(mainFileUri, completionLabel, expectedContent);
  }

  Future<void> test_unimportedSymbols_libraryImported_hidingOne() async {
    newFile(join(projectFolderPath, 'lib', 'my_classes.dart'), '''
class MyClass1 {}
class MyClass2 {}
''');

    content = '''
import 'package:test/my_classes.dart' hide MyClass1;
void f() {
  MyClas^
}
''';

    var expectedContent = '''
import 'package:test/my_classes.dart';
void f() {
  MyClass1
}
''';

    var completionLabel = 'MyClass1';

    await _checkCompletionEdits(mainFileUri, completionLabel, expectedContent);
  }

  Future<void> test_unimportedSymbols_libraryImported_showingOther() async {
    newFile(join(projectFolderPath, 'lib', 'my_classes.dart'), '''
class MyClass1 {}
class MyClass2 {}
''');

    content = '''
import 'package:test/my_classes.dart' show MyClass2;
void f() {
  MyClas^
}
''';

    var expectedContent = '''
import 'package:test/my_classes.dart' show MyClass1, MyClass2;
void f() {
  MyClass1
}
''';

    var completionLabel = 'MyClass1';

    await _checkCompletionEdits(mainFileUri, completionLabel, expectedContent);
  }

  // Code completion doesn't include prefixes for auto-imports so when an
  // auto-import is added it must be unprefixed even if the library exists with
  // a prefix (we cannot modify the inserted text during resolve).
  Future<void> test_unimportedSymbols_libraryImported_withPrefix() async {
    newFile(join(projectFolderPath, 'lib', 'my_classes.dart'), '''
class MyClass1 {}
class MyClass2 {}
''');

    content = '''
import 'package:test/my_classes.dart' as p1 show MyClass2;
void f() {
  MyClas^
}
''';

    var expectedContent = '''
import 'package:test/my_classes.dart' as p1 show MyClass2;
import 'package:test/my_classes.dart';
void f() {
  MyClass1
}
''';

    var completionLabel = 'MyClass1';

    await _checkCompletionEdits(mainFileUri, completionLabel, expectedContent);
  }

  /// This test reproduces a bug where the pathKey hash used in
  /// available_declarations.dart would not change with the contents of the file
  /// (as it always used 0 as the modification stamp) which would prevent
  /// completion including items from files that were open (had overlays).
  /// https://github.com/Dart-Code/Dart-Code/issues/2286#issuecomment-658597532
  Future<void> test_unimportedSymbols_modifiedFiles() async {
    var otherFilePath = join(projectFolderPath, 'lib', 'other_file.dart');
    var otherFileUri = pathContext.toUri(otherFilePath);

    content = 'MyOtherClass^';
    await initialize();
    await openFile(mainFileUri, code.code);
    await initialAnalysis;

    // Start with a blank file.
    newFile(otherFilePath, '');
    await openFile(otherFileUri, '');
    await pumpEventQueue(times: 5000);

    // Reopen the file with a class definition.
    await closeFile(otherFileUri);
    await openFile(otherFileUri, 'class MyOtherClass {}');
    await pumpEventQueue(times: 5000);

    // Ensure the class appears in completion.
    var completions = await getCompletion(mainFileUri, code.position.position);
    var matching = completions.where((c) => c.label == 'MyOtherClass').toList();
    expect(matching, hasLength(1));
  }

  Future<void> test_unimportedSymbols_namedConstructors() async {
    newFile(join(projectFolderPath, 'other_file.dart'), '''
/// This class is in another file.
class InOtherFile {
  InOtherFile.fromJson() {}
}
''');

    content = '''
void f() {
  var a = InOtherF^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    // Find the completion for the class in the other file.
    var completion = res.singleWhere(
      (c) => c.label == 'InOtherFile.fromJson()',
    );
    expect(completion, isNotNull);
    expect(completion.textEdit, isNotNull);

    // Expect no docs, this is added during resolve.
    expectDocumentation(completion, isNull);

    // Resolve the completion item (via server) to get any additional edits.
    // This is LSP's equiv of getSuggestionDetails() and is invoked by LSP
    // clients to populate additional info (in our case, any additional edits
    // for inserting the import).
    var resolved = await resolveCompletion(completion);
    expect(resolved, isNotNull);

    // Apply both the main completion edit and the additionalTextEdits atomically.
    var newContent = applyTextEdits(
      code.code,
      [
        toTextEdit(resolved.textEdit!),
      ].followedBy(resolved.additionalTextEdits!).toList(),
    );

    // Ensure both edits were made - the completion, and the inserted import.
    expect(
      newContent,
      equalsNormalized('''
import '../other_file.dart';

void f() {
  var a = InOtherFile.fromJson
}
'''),
    );
  }

  Future<void> test_unimportedSymbols_overrides() async {
    newFile(join(projectFolderPath, 'lib', 'a.dart'), 'class A {}');
    newFile(join(projectFolderPath, 'lib', 'b.dart'), 'class B {}');
    newFile(join(projectFolderPath, 'lib', 'c.dart'), 'class C {}');
    newFile(join(projectFolderPath, 'lib', 'd.dart'), 'class D {}');

    newFile(join(projectFolderPath, 'lib', 'base.dart'), '''
import 'a.dart';
import 'b.dart';
import 'c.dart';
import 'd.dart';

abstract class Base {
  D? myMethod(A a, B b, C c) => null;
}
''');

    // A will already be imported
    // B will already be imported but with a prefix
    // C & D are not imported and need importing (return + parameter types)
    content = '''
import 'package:test/a.dart';
import 'package:test/b.dart' as b;
import 'package:test/base.dart';

class BaseImpl extends Base {
  myMet^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    var completion = res.singleWhere(
      (c) => c.label == 'override myMethod(A a, b.B b, C c) { … }',
    );
    var resolved = await resolveCompletion(completion);

    var newContent = applyTextEdits(
      code.code,
      [
        toTextEdit(resolved.textEdit!),
      ].followedBy(resolved.additionalTextEdits!).toList(),
    );

    expect(
      newContent,
      equalsNormalized('''
import 'package:test/a.dart';
import 'package:test/b.dart' as b;
import 'package:test/base.dart';
import 'package:test/c.dart';
import 'package:test/d.dart';

class BaseImpl extends Base {
  @override
  D? myMethod(A a, b.B b, C c) {
    // TODO: implement myMethod
    return super.myMethod(a, b, c);
  }
}
'''),
    );
  }

  Future<void>
  test_unimportedSymbols_preferRelativeImportsLib_insideLib() async {
    _enableLints([LintNames.prefer_relative_imports]);
    var importingFilePath = join(
      projectFolderPath,
      'lib',
      'nested1',
      'main.dart',
    );
    var importingFileUri = pathContext.toUri(importingFilePath);
    var importedFilePath = join(
      projectFolderPath,
      'lib',
      'nested2',
      'imported.dart',
    );

    // Create a file that will be auto-imported from completion.
    newFile(importedFilePath, 'class MyClass {}');

    content = '''
void f() {
  MyClas^
}
''';

    var expectedContent = '''
import '../nested2/imported.dart';

void f() {
  MyClass
}
''';

    var completionLabel = 'MyClass';

    await _checkCompletionEdits(
      importingFileUri,
      completionLabel,
      expectedContent,
    );
  }

  Future<void>
  test_unimportedSymbols_preferRelativeImportsLib_outsideLib() async {
    // Files outside of the lib folder should still get absolute imports to
    // files inside lib, even with the lint enabled.
    _enableLints([LintNames.prefer_relative_imports]);
    var importingFilePath = join(
      projectFolderPath,
      'bin',
      'nested1',
      'main.dart',
    );
    var importingFileUri = pathContext.toUri(importingFilePath);
    var importedFilePath = join(
      projectFolderPath,
      'lib',
      'nested2',
      'imported.dart',
    );

    // Create a file that will be auto-imported from completion.
    newFile(importedFilePath, 'class MyClass {}');

    content = '''
void f() {
  MyClas^
}
''';

    var expectedContent = '''
import 'package:test/nested2/imported.dart';

void f() {
  MyClass
}
''';

    var completionLabel = 'MyClass';

    await _checkCompletionEdits(
      importingFileUri,
      completionLabel,
      expectedContent,
    );
  }

  Future<void> test_unimportedSymbols_unavailableIfDisabled() async {
    newFile(join(projectFolderPath, 'other_file.dart'), 'class InOtherFile {}');

    content = '''
void f() {
  InOtherF^
}
''';

    // applyEdit is supported in setUp, but explicitly disable the suggestions.
    await initialize(
      initializationOptions: {
        ...?defaultInitializationOptions,
        'suggestFromUnimportedLibraries': false,
      },
    );

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    // Ensure the item doesn't appear in the results (because we might not
    // be able to execute the import edits if they're in another file).
    var completion = res.singleWhereOrNull((c) => c.label == 'InOtherFile');
    expect(completion, isNull);
  }

  Future<void> test_unimportedSymbols_unavailableWithoutApplyEdit() async {
    // If client doesn't advertise support for workspace/applyEdit, we won't
    // include suggestion sets.
    setApplyEditSupport(false);

    newFile(join(projectFolderPath, 'other_file.dart'), 'class InOtherFile {}');

    content = '''
void f() {
  InOtherF^
}
''';

    await initialize();

    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(mainFileUri, code.position.position);

    // Ensure the item doesn't appear in the results (because we might not
    // be able to execute the import edits if they're in another file).
    var completion = res.singleWhereOrNull((c) => c.label == 'InOtherFile');
    expect(completion, isNull);
  }

  Future<void> test_unopenFile() async {
    content = '''
class MyClass {
  String abcdefghij;
}

void f() {
  MyClass a;
  a.abc^
}
''';

    newFile(mainFilePath, code.code);
    await initialize();
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.label == 'abcdefghij'), isTrue);
    var item = res.singleWhere((c) => c.label == 'abcdefghij');
    expect(
      item.insertTextFormat,
      anyOf(equals(InsertTextFormat.PlainText), isNull),
    );
    expect(item.insertText, anyOf(equals('abcdefghij'), isNull));
    var updated = applyTextEdits(code.code, [toTextEdit(item.textEdit!)]);
    expect(updated, contains('a.abcdefghij'));
  }

  /// Sets up the server with a file containing [content] and checks that
  /// accepting a specific completion produces [expectedContent].
  ///
  /// [content] should contain a `^` at the location where completion should be
  /// invoked/accepted.
  ///
  /// [expectedContent] can contain LSP snippet syntax if supported and expected
  /// in the result.
  Future<void> _checkCompletionEdits(
    Uri fileUri,
    String completionLabel,
    String expectedContent,
  ) async {
    await initialize();
    await openFile(fileUri, code.code);
    await initialAnalysis;
    var res = await getCompletion(fileUri, code.position.position);

    var completion = res.singleWhere((c) => c.label == completionLabel);
    var resolvedCompletion = await resolveCompletion(completion);

    // Apply both the main completion edit and the additionalTextEdits atomically.
    var newContent = applyTextEdits(
      code.code,
      [
        toTextEdit(resolvedCompletion.textEdit!),
      ].followedBy(resolvedCompletion.additionalTextEdits ?? []).toList(),
    );

    expect(newContent, equalsNormalized(expectedContent));
  }

  Future<void> _checkResultsForTriggerCharacters(
    String content,
    List<String> triggerCharacters,
    Matcher expectedResults,
  ) async {
    await initialize();
    await openFile(mainFileUri, code.code);

    for (var triggerCharacter in triggerCharacters) {
      var context = CompletionContext(
        triggerKind: CompletionTriggerKind.TriggerCharacter,
        triggerCharacter: triggerCharacter,
      );
      var res = await getCompletion(
        mainFileUri,
        code.position.position,
        context: context,
      );
      expect(
        res,
        expectedResults,
        reason: 'No completions when using $triggerCharacter',
      );
    }
  }
}

@reflectiveTest
class DartSnippetCompletionTest extends SnippetCompletionTest {
  Future<void> test_snippets_class() async {
    content = '''
clas^
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: ClassDeclaration.prefix,
      label: ClassDeclaration.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
class ${1:ClassName} {
  $0
}
'''),
    );
  }

  /// Checks that the `enableSnippets` setting can disable snippets even if the
  /// client supports them.
  Future<void> test_snippets_disabled() async {
    content = '^';

    // Support is set in setUp, but here we disable the user preference.
    await provideConfig(initialize, {'enableSnippets': false});

    await expectNoSnippets(content);
  }

  Future<void> test_snippets_doWhile() async {
    content = '''
void f() {
  do^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: DoStatement.prefix,
      label: DoStatement.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
void f() {
  do {
    $0
  } while (${1:condition});
}
'''),
    );
  }

  /// Snippets completions may abort if documents are modified (because they
  /// need to obtain resolved units when building edits) but they should not
  /// prevent non-Snippet completion results from being returned (because this
  /// happens frequently while typing).
  Future<void> test_snippets_failureDoesNotPreventNonSnippets() async {
    content = '''
void f() {
  ^
}
''';

    await initialize();
    await openFile(mainFileUri, code.code);
    await initialAnalysis;

    // Use a Completer to control when the completion handler starts computing.
    var completer = Completer<void>();
    CompletionHandler.delayAfterResolveForTests = completer.future;
    try {
      // Start the completion request but don't await it yet.
      var completionRequest = getCompletionList(
        mainFileUri,
        code.position.position,
      );
      // Modify the document to ensure the snippet requests will fail to build
      // edits and then allow the handler to continue.
      await replaceFile(222, mainFileUri, '');
      completer.complete();

      // Wait for the results.
      var result = await completionRequest;

      expect(result.items, isNotEmpty);
    } finally {
      // Ensure we never leave an incomplete future if anything above throws.
      CompletionHandler.delayAfterResolveForTests = null;
    }
  }

  Future<void>
  test_snippets_flutterStateless_notAvailable_notFlutterProject() async {
    content = '''
class A {}

stle^

class B {}
''';

    await initialize();
    await expectNoSnippet(content, FlutterStatelessWidget.prefix);
  }

  Future<void> test_snippets_for() async {
    content = '''
void f() {
  for^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: ForStatement.prefix,
      label: ForStatement.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
void f() {
  for (var i = 0; i < ${1:count}; i++) {
    $0
  }
}
'''),
    );
  }

  Future<void> test_snippets_forIn() async {
    content = '''
void f() {
  forin^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: ForInStatement.prefix,
      label: ForInStatement.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
void f() {
  for (var ${1:element} in ${2:collection}) {
    $0
  }
}
'''),
    );
  }

  Future<void> test_snippets_functionClassMember() async {
    content = '''
class A {
  fun^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: FunctionDeclaration.prefix,
      label: FunctionDeclaration.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
class A {
  ${1:void} ${2:name}(${3:params}) {
    $0
  }
}
'''),
    );
  }

  Future<void> test_snippets_functionNested() async {
    content = '''
void a() {
  fun^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: FunctionDeclaration.prefix,
      label: FunctionDeclaration.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
void a() {
  ${1:void} ${2:name}(${3:params}) {
    $0
  }
}
'''),
    );
  }

  Future<void> test_snippets_functionTopLevel() async {
    content = '''
fun^
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: FunctionDeclaration.prefix,
      label: FunctionDeclaration.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
${1:void} ${2:name}(${3:params}) {
  $0
}
'''),
    );
  }

  Future<void> test_snippets_if() async {
    content = '''
void f() {
  if^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: IfStatement.prefix,
      label: IfStatement.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
void f() {
  if (${1:condition}) {
    $0
  }
}
'''),
    );
  }

  Future<void> test_snippets_ifElse() async {
    content = '''
void f() {
  if^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: IfElseStatement.prefix,
      label: IfElseStatement.label,
    );
    var fourSpaces = '    ';
    expect(
      updated,
      equalsNormalized('''
void f() {
  if (\${1:condition}) {
    \$0
  } else {
$fourSpaces
  }
}
'''),
    );
  }

  /// Fetch snippets with itemDefaults enabled to ensure we don't return any
  /// values that match the defaults and that using the default range applies
  /// correctly.
  Future<void> test_snippets_itemDefaults() async {
    setCompletionListDefaults([
      'editRange',
      'insertTextMode',
      'insertTextFormat',
    ]);
    content = '''
fu^
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: FunctionDeclaration.prefix,
      label: FunctionDeclaration.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
${1:void} ${2:name}(${3:params}) {
  $0
}
'''),
    );
  }

  Future<void> test_snippets_mainFunction() async {
    content = '''
class A {}

main^

class B {}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: MainFunction.prefix,
      label: MainFunction.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
class A {}

void main(List<String> args) {
  $0
}

class B {}
'''),
    );
  }

  Future<void> test_snippets_notSupported() async {
    content = '^';

    // If we don't send support for Snippet CompletionItem kinds, we don't
    // expect any snippets at all.
    setCompletionItemSnippetSupport(false);
    await initialize();

    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    expect(res.any((c) => c.kind == CompletionItemKind.Snippet), isFalse);
  }

  Future<void> test_snippets_switch() async {
    content = '''
void f() {
  swi^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: SwitchStatement.prefix,
      label: SwitchStatement.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
void f() {
  switch (${1:expression}) {
    case ${2:value}:
      $0
      break;
    default:
  }
}
'''),
    );
  }

  Future<void> test_snippets_testBlock() async {
    mainFilePath = join(projectFolderPath, 'test', 'foo_test.dart');
    content = '''
void f() {
  test^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: TestDefinition.prefix,
      label: TestDefinition.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
import 'package:test/test.dart';

void f() {
  test('${1:test name}', () {
    $0
  });
}
'''),
    );
  }

  Future<void> test_snippets_testGroupBlock() async {
    mainFilePath = join(projectFolderPath, 'test', 'foo_test.dart');
    content = '''
void f() {
  group^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: TestGroupDefinition.prefix,
      label: TestGroupDefinition.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
import 'package:test/test.dart';

void f() {
  group('${1:group name}', () {
    $0
  });
}
'''),
    );
  }

  Future<void> test_snippets_tryCatch() async {
    content = '''
void f() {
  tr^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: TryCatchStatement.prefix,
      label: TryCatchStatement.label,
    );
    var fourSpaces = '    ';
    expect(
      updated,
      equalsNormalized('''
void f() {
  try {
    \$0
  } catch (\${1:e}) {
$fourSpaces
  }
}
'''),
    );
  }

  Future<void> test_snippets_while() async {
    content = '''
void f() {
  while^
}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: WhileStatement.prefix,
      label: WhileStatement.label,
    );

    expect(
      updated,
      equalsNormalized(r'''
void f() {
  while (${1:condition}) {
    $0
  }
}
'''),
    );
  }
}

@reflectiveTest
class FlutterSnippetCompletionTest extends SnippetCompletionTest {
  /// Standard import statements expected for basic Widgets.
  String get expectedImports => '''
import 'package:flutter/widgets.dart';''';

  /// Constructor params expected on Widget classes.
  String get expectedWidgetConstructorParams => '({super.key})';

  @override
  void setUp() {
    super.setUp();
    writeTestPackageConfig(flutter: true);
  }

  Future<void> test_snippets_flutterStateful() async {
    content = '''
import 'package:flutter/widgets.dart';

class A {}

stful^

class B {}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: FlutterStatefulWidget.prefix,
      label: FlutterStatefulWidget.label,
    );

    expect(
      updated,
      equalsNormalized('''
import 'package:flutter/widgets.dart';

class A {}

class \${1:MyWidget} extends StatefulWidget {
  const new$expectedWidgetConstructorParams;

  @override
  State<\${1:MyWidget}> createState() => _\${1:MyWidget}State();
}

class _\${1:MyWidget}State extends State<\${1:MyWidget}> {
  @override
  Widget build(BuildContext context) {
    return \${0:const Placeholder()};
  }
}

class B {}
'''),
    );
  }

  Future<void> test_snippets_flutterStatefulWithAnimationController() async {
    content = '''
import 'package:flutter/widgets.dart';

class A {}

stanim^

class B {}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: FlutterStatefulWidgetWithAnimationController.prefix,
      label: FlutterStatefulWidgetWithAnimationController.label,
    );

    expect(
      updated,
      equalsNormalized('''
import 'package:flutter/widgets.dart';

class A {}

class \${1:MyWidget} extends StatefulWidget {
  const new$expectedWidgetConstructorParams;

  @override
  State<\${1:MyWidget}> createState() => _\${1:MyWidget}State();
}

class _\${1:MyWidget}State extends State<\${1:MyWidget}>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return \${0:const Placeholder()};
  }
}

class B {}
'''),
    );
  }

  Future<void> test_snippets_flutterStateless() async {
    content = '''
import 'package:flutter/widgets.dart';

class A {}

stle^

class B {}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: FlutterStatelessWidget.prefix,
      label: FlutterStatelessWidget.label,
    );

    expect(
      updated,
      equalsNormalized('''
import 'package:flutter/widgets.dart';

class A {}

class \${1:MyWidget} extends StatelessWidget {
  const new$expectedWidgetConstructorParams;

  @override
  Widget build(BuildContext context) {
    return \${0:const Placeholder()};
  }
}

class B {}
'''),
    );
  }

  Future<void> test_snippets_flutterStateless_addsImports() async {
    content = '''
class A {}

stle^

class B {}
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: FlutterStatelessWidget.prefix,
      label: FlutterStatelessWidget.label,
    );

    expect(
      updated,
      equalsNormalized('''
$expectedImports

class A {}

class \${1:MyWidget} extends StatelessWidget {
  const new$expectedWidgetConstructorParams;

  @override
  Widget build(BuildContext context) {
    return \${0:const Placeholder()};
  }
}

class B {}
'''),
    );
  }

  Future<void> test_snippets_flutterStateless_addsImports_onlyPrefix() async {
    content = '''
stless^
''';

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: FlutterStatelessWidget.prefix,
      label: FlutterStatelessWidget.label,
    );

    expect(
      updated,
      equalsNormalized('''
$expectedImports

class \${1:MyWidget} extends StatelessWidget {
  const new$expectedWidgetConstructorParams;

  @override
  Widget build(BuildContext context) {
    return \${0:const Placeholder()};
  }
}
'''),
    );
  }

  Future<void> test_snippets_flutterStateless_addsImports_zeroOffset() async {
    content = '''
^
'''; // Deliberate trailing newline to ensure imports aren't inserted at "end".

    await initialize();
    var updated = await expectAndApplySnippet(
      prefix: FlutterStatelessWidget.prefix,
      label: FlutterStatelessWidget.label,
    );

    expect(
      updated,
      equalsNormalized('''
$expectedImports

class \${1:MyWidget} extends StatelessWidget {
  const new$expectedWidgetConstructorParams;

  @override
  Widget build(BuildContext context) {
    return \${0:const Placeholder()};
  }
}
'''),
    );
  }

  Future<void> test_snippets_flutterStateless_notAvailable_notTopLevel() async {
    content = '''
class A {

  stle^

}
''';

    await initialize();
    await expectNoSnippet(content, FlutterStatelessWidget.prefix);
  }

  Future<void> test_snippets_flutterStateless_outsideAnalysisRoot() async {
    content = '''
stle^
''';

    await initialize();
    var otherFileUri = pathContext.toUri(convertPath('/other/file.dart'));
    await openFile(otherFileUri, code.code);
    var res = await getCompletion(otherFileUri, code.position.position);
    var snippetItems = res.where((c) => c.kind == CompletionItemKind.Snippet);
    expect(snippetItems, hasLength(0));
  }
}

abstract class SnippetCompletionTest extends AbstractLspAnalysisServerTest
    with CompletionTestMixin {
  late String content;
  late final TestCode code = TestCode.parseNormalized(content);

  /// Expect that there is a snippet for [prefix] at [position] with the label
  /// [label] and return the results of applying it to [content].
  Future<String> expectAndApplySnippet({
    required String prefix,
    required String label,
  }) async {
    var (snippet: snippet, defaults: defaults) = await expectSnippet(
      code,
      prefix: prefix,
      label: label,
    );

    var textEdit = snippet.textEdit;
    var textEditText = snippet.textEditText;
    var additionalTextEdits = snippet.additionalTextEdits;

    var edits = [
      // Additional TextEdits come first, because if they have the same offset
      // as edits in the normal edit, they will be inserted first.
      // https://github.com/microsoft/vscode/issues/143888.
      ...?additionalTextEdits,
      // Then we also either have an edit itself or we just have textEditText
      // and a default range.
      if (textEdit != null)
        toTextEdit(textEdit)
      else
        TextEdit(
          newText: textEditText!,
          range: defaults!.editRange!.map(
            (ranges) =>
                throw 'Unexpected snippet with different insert/replace ranges',
            (range) => range,
          ),
        ),
    ];

    // Also apply the edit and check that it went in the right place with the
    // correct formatting. Edit groups will just appear in the raw textmate
    // snippet syntax here, as we don't do any special handling of them (and
    // assume what's coded here is correct, and that the client will correctly
    // interpret them).
    var updated = applyTextEdits(code.code, edits);
    return updated;
  }

  /// Expect that there is no snippet for [prefix] at the position of `^` within
  /// [content].
  Future<void> expectNoSnippet(String content, String prefix) async {
    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var hasSnippet = res.any((c) => c.filterText == prefix);
    expect(hasSnippet, isFalse);
  }

  /// Expect that there are no snippets at the position of `^` within [content].
  Future<void> expectNoSnippets(String content) async {
    await openFile(mainFileUri, code.code);
    var res = await getCompletion(mainFileUri, code.position.position);
    var hasAnySnippet = res.any((c) => c.kind == CompletionItemKind.Snippet);
    expect(hasAnySnippet, isFalse);
  }

  /// Expect that there is a snippet for [prefix] with the label [label] at
  /// the marked position in [code].
  Future<({CompletionItem snippet, CompletionItemDefaults? defaults})>
  expectSnippet(
    TestCode code, {
    required String prefix,
    required String label,
  }) async {
    await openFile(mainFileUri, code.code);
    var res = await getCompletionList(mainFileUri, code.position.position);
    var item = res.items.singleWhere(
      (c) =>
          c.kind == CompletionItemKind.Snippet &&
          (c.filterText ?? c.label) == prefix &&
          c.label == label,
    );
    expect(item.insertTextFormat, InsertTextFormat.Snippet);
    expect(item.insertText, isNull);
    // We either expect textEdit (if we had a range) or textEditText (or we did
    // not), but never both.
    expect(item.textEdit == null, item.textEditText != null);
    expect(item.textEditText != null, res.itemDefaults?.editRange != null);
    return (snippet: item, defaults: res.itemDefaults);
  }

  @override
  void setUp() {
    super.setUp();

    // Snippet completion tests have incomplete code.
    failTestOnErrorDiagnostic = false;

    setCompletionItemSnippetSupport();
  }
}
