// Copyright (c) 2014, 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/src/protocol_server.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/test_utilities/platform.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:analyzer_testing/package_config_file_builder.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import 'analysis_server_base.dart';
import 'mocks.dart';
import 'services/completion/dart/completion_check.dart';
import 'services/completion/dart/completion_printer.dart' as printer;
import 'services/completion/dart/text_expectations.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(CompletionDomainHandlerGetSuggestionDetails2Test);
    defineReflectiveTests(CompletionDomainHandlerGetSuggestions2Test);
  });
}

@reflectiveTest
class CompletionDomainHandlerGetSuggestionDetails2Test
    extends PubPackageAnalysisServerTest {
  String get testEolEscaped =>
      testEol.replaceAll('\r', r'\r').replaceAll('\n', r'\n');

  void assertDetailsText(
    CompletionGetSuggestionDetails2Result result,
    String expected, {
    bool printIfFailed = true,
  }) {
    var buffer = StringBuffer();
    _SuggestionDetailsPrinter(
      resourceProvider: resourceProvider,
      fileDisplayMap: {testFile: 'testFile'},
      buffer: buffer,
      result: result,
    ).writeResult();
    var actual = buffer.toString();

    if (actual != expected) {
      if (printIfFailed) {
        print(actual);
      }
      TextExpectationsCollector.add(actual);
    }
    expect(actual, expected);
  }

  Future<void> test_alreadyImported() async {
    await _configureWithWorkspaceRoot();

    var details = await _getTestCodeDetails(
      '''
import 'dart:math';
void f() {
  Rand^
}
''',
      completion: 'Random',
      libraryUri: 'dart:math',
    );

    assertDetailsText(details, r'''
completion: Random
  change
''');
  }

  Future<void> test_import_dart() async {
    await _configureWithWorkspaceRoot();

    var details = await _getTestCodeDetails(
      '''
void f() {
  R^
}
''',
      completion: 'Random',
      libraryUri: 'dart:math',
    );

    assertDetailsText(details, '''
completion: Random
  change
    testFile
      offset: 0
      length: 0
      replacement: import 'dart:math';$testEolEscaped$testEolEscaped
''');
  }

  Future<void> test_import_package_dependencies() async {
    writeTestPackagePubspecYamlFile(r'''
name: test
dependencies:
  aaa: any
''');

    var aaaRoot = getFolder('$workspaceRootPath/packages/aaa');
    newFile('${aaaRoot.path}/lib/f.dart', '''
class Test {}
''');

    writeTestPackageConfig(
      config: PackageConfigFileBuilder()..add(name: 'aaa', rootFolder: aaaRoot),
    );

    await _configureWithWorkspaceRoot();

    var details = await _getTestCodeDetails(
      '''
void f() {
  T^
}
''',
      completion: 'Test',
      libraryUri: 'package:aaa/a.dart',
    );

    assertDetailsText(details, '''
completion: Test
  change
    testFile
      offset: 0
      length: 0
      replacement: import 'package:aaa/a.dart';$testEolEscaped$testEolEscaped
''');
  }

  Future<void> test_import_package_this() async {
    newFile('$testPackageLibPath/a.dart', '''
class Test {}
''');

    await _configureWithWorkspaceRoot();

    var details = await _getTestCodeDetails(
      '''
void f() {
  T^
}
''',
      completion: 'Test',
      libraryUri: 'package:test/a.dart',
    );

    assertDetailsText(details, '''
completion: Test
  change
    testFile
      offset: 0
      length: 0
      replacement: import 'package:test/a.dart';$testEolEscaped$testEolEscaped
''');
  }

  Future<void> test_invalidLibraryUri() async {
    await _configureWithWorkspaceRoot();

    var request = CompletionGetSuggestionDetails2Params(
      testFile.path,
      0,
      'Random',
      '[foo]:bar',
    ).toRequest('0', clientUriConverter: server.uriConverter);

    var response = await handleRequest(request);
    expect(response.error?.code, RequestErrorCode.INVALID_PARAMETER);
    // TODO(scheglov): Check that says "libraryUri".
  }

  Future<void> test_invalidPath() async {
    await _configureWithWorkspaceRoot();

    var request = CompletionGetSuggestionDetails2Params(
      'foo',
      0,
      'Random',
      'dart:math',
    ).toRequest('0', clientUriConverter: server.uriConverter);

    var response = await handleRequest(request);
    expect(response.error?.code, RequestErrorCode.INVALID_FILE_PATH_FORMAT);
  }

  Future<void> _configureWithWorkspaceRoot() async {
    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;
  }

  Future<CompletionGetSuggestionDetails2Result> _getCodeDetails({
    required String path,
    required String content,
    required String completion,
    required String libraryUri,
  }) async {
    var code = TestCode.parse(content);
    var completionOffset = code.position.offset;

    newFile(path, code.code);

    return await _getDetails(
      path: path,
      completionOffset: completionOffset,
      completion: completion,
      libraryUri: libraryUri,
    );
  }

  Future<CompletionGetSuggestionDetails2Result> _getDetails({
    required String path,
    required int completionOffset,
    required String completion,
    required String libraryUri,
  }) async {
    var request = CompletionGetSuggestionDetails2Params(
      path,
      completionOffset,
      completion,
      libraryUri,
    ).toRequest('0', clientUriConverter: server.uriConverter);

    var response = await handleSuccessfulRequest(request);
    return CompletionGetSuggestionDetails2Result.fromResponse(
      response,
      clientUriConverter: server.uriConverter,
    );
  }

  Future<CompletionGetSuggestionDetails2Result> _getTestCodeDetails(
    String content, {
    required String completion,
    required String libraryUri,
  }) {
    return _getCodeDetails(
      path: convertPath(testFilePath),
      content: content,
      completion: completion,
      libraryUri: libraryUri,
    );
  }
}

@reflectiveTest
class CompletionDomainHandlerGetSuggestions2Test
    extends PubPackageAnalysisServerTest {
  printer.Configuration printerConfiguration = printer.Configuration(
    filter: (suggestion) {
      var completion = suggestion.completion;
      if (completion.startsWith('A0')) {
        return suggestion.isClass;
      }
      return const {'foo0'}.any(completion.startsWith);
    },
    withIsNotImported: true,
    withLibraryUri: true,
  );

  /// Asserts that the [response] has the [expected] textual dump produced
  /// using [printerConfiguration].
  void assertResponseText(
    CompletionResponseForTesting response,
    String expected, {
    bool printIfFailed = true,
  }) {
    // The generated response will always be using the EOLs being tested so
    // normalize the expected test to the same to ensure that all actual output
    // uses the expected line endings.
    expected = normalizeNewlinesForPlatform(expected);

    var buffer = StringBuffer();
    printer.CompletionResponsePrinter(
      buffer: buffer,
      configuration: printerConfiguration,
      response: response,
    ).writeResponse();
    var actual = buffer.toString();

    // The source code in our test files will always be written with only `\n`
    // (due to git settings) so strip any `\r` that might be in the actual
    // result for any output intended to go into source files.
    var actualForSourceCode = actual.replaceAll('\r', '');

    if (actual != expected) {
      if (printIfFailed) {
        print(actualForSourceCode);
      }
      TextExpectationsCollector.add(actualForSourceCode);
    }
    expect(actual, expected);
  }

  @override
  void setUp() {
    super.setUp();
    server.completionState.budgetDuration = const Duration(seconds: 30);
  }

  Future<void> test_abort_onAnotherCompletionRequest() async {
    var abortedIdSet = <String>{};
    server.discardedRequests.stream.listen((request) {
      abortedIdSet.add(request.id);
    });

    newFile(testFilePath, '');

    await _configureWithWorkspaceRoot();

    // Send three requests, the first two should be aborted.
    var request0 = _sendTestCompletionRequest('0', 0);
    var request1 = _sendTestCompletionRequest('1', 0);
    var request2 = _sendTestCompletionRequest('2', 0);

    // Wait for all three.
    var response0 = await request0.toResponse();
    var response1 = await request1.toResponse();
    var response2 = await request2.toResponse();

    // The first two should be aborted.
    expect(abortedIdSet, {'0', '1'});

    printerConfiguration.filter = (suggestion) {
      if (suggestion.isClass) {
        return const {'int'}.contains(suggestion.completion);
      }
      return false;
    };

    assertResponseText(response0, r'''
suggestions
''');

    assertResponseText(response1, r'''
suggestions
''');

    assertResponseText(response2, r'''
suggestions
  int
    kind: class
    isNotImported: null
    libraryUri: dart:core
''');
  }

  Future<void> test_abort_onUpdateContent() async {
    var abortedIdSet = <String>{};
    server.discardedRequests.stream.listen((request) {
      abortedIdSet.add(request.id);
    });

    newFile(testFilePath, '');

    await _configureWithWorkspaceRoot();

    // Schedule a completion request.
    var request = _sendTestCompletionRequest('0', 0);

    // Simulate typing in the IDE.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        testFile.path: AddContentOverlay('void f() {}'),
      }).toRequest('1', clientUriConverter: server.uriConverter),
    );

    // The request should be aborted.
    var response = await request.toResponse();
    expect(abortedIdSet, {'0'});

    assertResponseText(response, r'''
suggestions
''');
  }

  Future<void> test_applyPendingFileChanges() async {
    await _configureWithWorkspaceRoot();

    // Request with the empty content.
    await _getTestCodeSuggestions('^');

    // Change the file, and request again.
    // Should apply pending file changes before resolving.
    var response = await _getTestCodeSuggestions('Str^');

    printerConfiguration.filter = (suggestion) {
      if (suggestion.isClass) {
        return const {'String'}.contains(suggestion.completion);
      }
      return false;
    };

    assertResponseText(response, r'''
replacement
  left: 3
suggestions
  String
    kind: class
    isNotImported: null
    libraryUri: dart:core
''');
  }

  Future<void> test_isNotImportedFeature_prefixed_classInstanceMethod() async {
    newFile('$testPackageLibPath/a.dart', '''
class A {
  void foo01() {}
}
''');

    newFile('$testPackageLibPath/b.dart', '''
import 'a.dart';

class B extends A {
  void foo02() {}
}
''');

    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
import 'b.dart';

void f(B b) {
  b.foo0^
}
''');

    // The fact that `b.dart` is imported, and `a.dart` is not, does not affect
    // the order of suggestions added with an expression prefix. We are not
    // going to import anything, so this does not matter.
    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
  foo02
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_notImported_dart() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
void f() {
  Rand^
}
''');

    printerConfiguration.filter = (suggestion) {
      return suggestion.isClass;
    };

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  Random
    kind: class
    isNotImported: true
    libraryUri: dart:math
''');
  }

  Future<void> test_notImported_emptyBudget() async {
    await _configureWithWorkspaceRoot();

    // Empty budget, so no not yet imported libraries.
    server.completionState.budgetDuration = const Duration();

    var response = await _getTestCodeSuggestions('''
void f() {
  Rand^
}
''');

    printerConfiguration.filter = (_) => true;

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
''');
  }

  Future<void> test_notImported_lowerRelevance_extension_getter() async {
    await _configureWithWorkspaceRoot();
    printerConfiguration.sorting =
        printer.Sorting.relevanceThenCompletionThenKind;

    newFile('$testPackageLibPath/a.dart', '''
extension E1 on int {
  int get foo01 => 0;
}
''');

    newFile('$testPackageLibPath/b.dart', '''
extension E2 on int {
  int get foo02 => 0;
}
''');

    var response = await _getTestCodeSuggestions(r'''
import 'b.dart';

void f() {
  0.foo0^
}
''');

    // `foo01` relevance is decreased because it is not yet imported.
    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo02
    kind: getter
    isNotImported: null
    libraryUri: null
  foo01
    kind: getter
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_notImported_lowerRelevance_extension_method() async {
    await _configureWithWorkspaceRoot();
    printerConfiguration.sorting =
        printer.Sorting.relevanceThenCompletionThenKind;

    newFile('$testPackageLibPath/a.dart', '''
extension E1 on int {
  void foo01() {}
}
''');

    newFile('$testPackageLibPath/b.dart', '''
extension E2 on int {
  void foo02() {}
}
''');

    var response = await _getTestCodeSuggestions(r'''
import 'b.dart';

void f() {
  0.foo0^
}
''');

    // `foo01` relevance is decreased because it is not yet imported.
    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo02
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
  foo01
    kind: methodInvocation
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_notImported_lowerRelevance_extension_setter() async {
    await _configureWithWorkspaceRoot();
    printerConfiguration.sorting =
        printer.Sorting.relevanceThenCompletionThenKind;

    newFile('$testPackageLibPath/a.dart', '''
extension E1 on int {
  set foo01(int _) {}
}
''');

    newFile('$testPackageLibPath/b.dart', '''
extension E2 on int {
  set foo02(int _) {}
}
''');

    var response = await _getTestCodeSuggestions(r'''
import 'b.dart';

void f() {
  0.foo0^
}
''');

    // `foo01` relevance is decreased because it is not yet imported.
    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo02
    kind: setter
    isNotImported: null
    libraryUri: null
  foo01
    kind: setter
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_notImported_lowerRelevance_topLevel_class() async {
    newFile('$testPackageLibPath/a.dart', '''
class A01 {}
''');

    newFile('$testPackageLibPath/b.dart', '''
class A02 {}
''');

    await _configureWithWorkspaceRoot();
    printerConfiguration.sorting =
        printer.Sorting.relevanceThenCompletionThenKind;

    var response = await _getTestCodeSuggestions('''
import 'b.dart';

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

    // `A01` relevance is decreased because it is not yet imported.
    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A02
    kind: class
    isNotImported: null
    libraryUri: package:test/b.dart
  A01
    kind: class
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_notImported_lowerRelevance_topLevel_getter() async {
    newFile('$testPackageLibPath/a.dart', '''
int get foo01 => 0;
''');

    newFile('$testPackageLibPath/b.dart', '''
int get foo02 => 0;
''');

    await _configureWithWorkspaceRoot();
    printerConfiguration.sorting =
        printer.Sorting.relevanceThenCompletionThenKind;

    var response = await _getTestCodeSuggestions('''
import 'b.dart';

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

    // `foo01` relevance is decreased because it is not yet imported.
    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo02
    kind: getter
    isNotImported: null
    libraryUri: package:test/b.dart
  foo01
    kind: getter
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_notImported_lowerRelevance_topLevel_setter() async {
    newFile('$testPackageLibPath/a.dart', '''
set foo01(int _) {}
''');

    newFile('$testPackageLibPath/b.dart', '''
set foo02(int _) {}
''');

    await _configureWithWorkspaceRoot();
    printerConfiguration.sorting =
        printer.Sorting.relevanceThenCompletionThenKind;

    var response = await _getTestCodeSuggestions('''
import 'b.dart';

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

    // `foo01` relevance is decreased because it is not yet imported.
    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo02
    kind: setter
    isNotImported: null
    libraryUri: package:test/b.dart
  foo01
    kind: setter
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_notImported_lowerRelevance_topLevel_variable() async {
    newFile('$testPackageLibPath/a.dart', '''
var foo01 = 0;
''');

    newFile('$testPackageLibPath/b.dart', '''
var foo02 = 0;
''');

    await _configureWithWorkspaceRoot();
    printerConfiguration.sorting =
        printer.Sorting.relevanceThenCompletionThenKind;

    var response = await _getTestCodeSuggestions('''
import 'b.dart';

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

    // `foo01` relevance is decreased because it is not yet imported.
    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo02
    kind: topLevelVariable
    isNotImported: null
    libraryUri: package:test/b.dart
  foo01
    kind: topLevelVariable
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_notImported_pub_dependencies_inBin() async {
    writeTestPackagePubspecYamlFile(r'''
name: test
dependencies:
  aaa: any
dev_dependencies:
  bbb: any
''');

    var aaaRoot = getFolder('$workspaceRootPath/packages/aaa');
    newFile('${aaaRoot.path}/lib/f.dart', '''
class A01 {}
''');

    var bbbRoot = getFolder('$workspaceRootPath/packages/bbb');
    newFile('${bbbRoot.path}/lib/f.dart', '''
class A02 {}
''');

    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'aaa', rootFolder: aaaRoot)
        ..add(name: 'bbb', rootFolder: bbbRoot),
    );

    await _configureWithWorkspaceRoot();

    var binPath = convertPath('$testPackageRootPath/bin/main.dart');
    var response = await _getCodeSuggestions(
      path: binPath,
      content: '''
void f() {
  A0^
}
''',
    );

    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: true
    libraryUri: package:aaa/f.dart
''');
  }

  Future<void> test_notImported_pub_dependencies_inLib() async {
    writeTestPackagePubspecYamlFile(r'''
name: test
dependencies:
  aaa: any
dev_dependencies:
  bbb: any
''');

    var aaaRoot = getFolder('$workspaceRootPath/packages/aaa');
    newFile('${aaaRoot.path}/lib/f.dart', '''
class A01 {}
''');
    newFile('${aaaRoot.path}/lib/src/f.dart', '''
class A02 {}
''');

    var bbbRoot = getFolder('$workspaceRootPath/packages/bbb');
    newFile('${bbbRoot.path}/lib/f.dart', '''
class A03 {}
''');
    newFile('${bbbRoot.path}/lib/src/f.dart', '''
class A04 {}
''');

    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'aaa', rootFolder: aaaRoot)
        ..add(name: 'bbb', rootFolder: bbbRoot),
    );

    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
void f() {
  A0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: true
    libraryUri: package:aaa/f.dart
''');
  }

  Future<void> test_notImported_pub_dependencies_inTest() async {
    writeTestPackagePubspecYamlFile(r'''
name: test
dependencies:
  aaa: any
dev_dependencies:
  bbb: any
''');

    var aaaRoot = getFolder('$workspaceRootPath/packages/aaa');
    newFile('${aaaRoot.path}/lib/f.dart', '''
class A01 {}
''');
    newFile('${aaaRoot.path}/lib/src/f.dart', '''
class A02 {}
''');

    var bbbRoot = getFolder('$workspaceRootPath/packages/bbb');
    newFile('${bbbRoot.path}/lib/f.dart', '''
class A03 {}
''');
    newFile('${bbbRoot.path}/lib/src/f.dart', '''
class A04 {}
''');

    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'aaa', rootFolder: aaaRoot)
        ..add(name: 'bbb', rootFolder: bbbRoot),
    );

    await _configureWithWorkspaceRoot();

    var test_path = convertPath('$testPackageTestPath/test.dart');
    var response = await _getCodeSuggestions(
      path: test_path,
      content: '''
void f() {
  A0^
}
''',
    );

    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: true
    libraryUri: package:aaa/f.dart
  A03
    kind: class
    isNotImported: true
    libraryUri: package:bbb/f.dart
''');
  }

  Future<void> test_notImported_pub_dependencies_inWeb() async {
    writeTestPackagePubspecYamlFile(r'''
name: test
dependencies:
  aaa: any
dev_dependencies:
  bbb: any
''');

    var aaaRoot = getFolder('$workspaceRootPath/packages/aaa');
    newFile('${aaaRoot.path}/lib/f.dart', '''
class A01 {}
''');

    var bbbRoot = getFolder('$workspaceRootPath/packages/bbb');
    newFile('${bbbRoot.path}/lib/f.dart', '''
class A02 {}
''');

    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'aaa', rootFolder: aaaRoot)
        ..add(name: 'bbb', rootFolder: bbbRoot),
    );

    await _configureWithWorkspaceRoot();

    var webPath = convertPath('$testPackageRootPath/web/main.dart');
    var response = await _getCodeSuggestions(
      path: webPath,
      content: '''
void f() {
  A0^
}
''',
    );

    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: true
    libraryUri: package:aaa/f.dart
''');
  }

  Future<void> test_notImported_pub_this() async {
    newFile('$testPackageLibPath/a.dart', '''
class A01 {}
''');

    newFile('$testPackageLibPath/b.dart', '''
class A02 {}
''');

    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
void f() {
  A0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: true
    libraryUri: package:test/a.dart
  A02
    kind: class
    isNotImported: true
    libraryUri: package:test/b.dart
''');
  }

  Future<void> test_notImported_pub_this_hasImport() async {
    newFile('$testPackageLibPath/a.dart', '''
class A01 {}
class A02 {}
''');

    newFile('$testPackageLibPath/b.dart', '''
class A03 {}
''');

    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
import 'a.dart';

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

    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: null
    libraryUri: package:test/a.dart
  A02
    kind: class
    isNotImported: null
    libraryUri: package:test/a.dart
  A03
    kind: class
    isNotImported: true
    libraryUri: package:test/b.dart
''');
  }

  Future<void> test_notImported_pub_this_hasImport_hasShow() async {
    newFile('$testPackageLibPath/a.dart', '''
class A01 {}
class A02 {}
''');

    newFile('$testPackageLibPath/b.dart', '''
class A03 {}
''');

    await _configureWithWorkspaceRoot();
    printerConfiguration.sorting =
        printer.Sorting.relevanceThenCompletionThenKind;

    var response = await _getTestCodeSuggestions('''
import 'a.dart' show A02;

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

    // Note:
    // 1. A02 is the first, because it is already imported.
    // 2. A01 is still suggested, but with lower relevance.
    // 3. A03 has the same relevance (not tested), but sorted by name.
    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A02
    kind: class
    isNotImported: null
    libraryUri: package:test/a.dart
  A01
    kind: class
    isNotImported: true
    libraryUri: package:test/a.dart
  A03
    kind: class
    isNotImported: true
    libraryUri: package:test/b.dart
''');
  }

  Future<void> test_notImported_pub_this_inLib_excludesTest() async {
    writeTestPackagePubspecYamlFile(r'''
name: test
''');

    newFile('$testPackageLibPath/a.dart', '''
class A01 {}
''');

    newFile('$testPackageTestPath/b.dart', '''
class A02 {}
''');

    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
void f() {
  A0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_notImported_pub_this_inLib_includesThisSrc() async {
    writeTestPackagePubspecYamlFile(r'''
name: test
''');

    newFile('$testPackageLibPath/f.dart', '''
class A01 {}
''');

    newFile('$testPackageLibPath/src/f.dart', '''
class A02 {}
''');

    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
void f() {
  A0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: true
    libraryUri: package:test/f.dart
  A02
    kind: class
    isNotImported: true
    libraryUri: package:test/src/f.dart
''');
  }

  Future<void> test_notImported_pub_this_inTest_includesTest() async {
    writeTestPackagePubspecYamlFile(r'''
name: test
''');

    newFile('$testPackageLibPath/a.dart', '''
class A01 {}
''');

    var b = newFile('$testPackageTestPath/b.dart', '''
class A02 {}
''');
    var b_uriStr = toUriStr(b.path);

    await _configureWithWorkspaceRoot();

    var test_path = convertPath('$testPackageTestPath/test.dart');
    var response = await _getCodeSuggestions(
      path: test_path,
      content: '''
void f() {
  A0^
}
''',
    );

    assertResponseText(response, '''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: true
    libraryUri: package:test/a.dart
  A02
    kind: class
    isNotImported: true
    libraryUri: $b_uriStr
''');
  }

  Future<void> test_notImported_pub_this_inTest_includesThisSrc() async {
    writeTestPackagePubspecYamlFile(r'''
name: test
''');

    newFile('$testPackageLibPath/f.dart', '''
class A01 {}
''');

    newFile('$testPackageLibPath/src/f.dart', '''
class A02 {}
''');

    await _configureWithWorkspaceRoot();

    var test_path = convertPath('$testPackageTestPath/test.dart');
    var response = await _getCodeSuggestions(
      path: test_path,
      content: '''
void f() {
  A0^
}
''',
    );

    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: true
    libraryUri: package:test/f.dart
  A02
    kind: class
    isNotImported: true
    libraryUri: package:test/src/f.dart
''');
  }

  Future<void> test_numResults_class_methods() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
class A {
  void foo01() {}
  void foo02() {}
  void foo03() {}
}

void f(A a) {
  a.foo0^
}
''', maxResults: 2);

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
  foo02
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_numResults_includesLocalVariable() async {
    await _configureWithWorkspaceRoot();

    // Create many imported declarations to exceed the maxResults cap.
    newFile(
      '$testPackageLibPath/a.dart',
      [
        for (var i = 1; i <= 50; i++) 'class C$i {}',
        for (var i = 1; i <= 50; i++) 'void f$i() {}',
        for (var i = 1; i <= 50; i++) 'var v$i = $i;',
      ].join('\n'),
    );

    // No prefix, no filtering.
    // Local variables 'foo0*' must be included.
    var response = await _getTestCodeSuggestions('''
import 'a.dart';
void f() {
  var foo01 = 0;
  var foo02 = 0;
  ^
}
''', maxResults: 10);

    // Only include local variables to keep the result stable.
    printerConfiguration.filter = (suggestion) {
      return suggestion.completion.startsWith('foo0');
    };

    // Note the variables order: the closer, the higher.
    assertResponseText(response, r'''
suggestions
  foo02
    kind: localVariable
    isNotImported: null
    libraryUri: null
  foo01
    kind: localVariable
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_numResults_topLevelVariables() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
var foo01 = 0;
var foo02 = 0;
var foo03 = 0;

void f() {
  foo0^
}
''', maxResults: 2);

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: topLevelVariable
    isNotImported: null
    libraryUri: null
  foo02
    kind: topLevelVariable
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_numResults_topLevelVariables_imported_withPrefix() async {
    await _configureWithWorkspaceRoot();

    newFile('$testPackageLibPath/a.dart', '''
var foo01 = 0;
var foo02 = 0;
var foo03 = 0;
''');

    var response = await _getTestCodeSuggestions('''
import 'a.dart' as prefix;

void f() {
  prefix.^
}
''', maxResults: 2);

    assertResponseText(response, r'''
suggestions
  foo01
    kind: topLevelVariable
    isNotImported: null
    libraryUri: package:test/a.dart
  foo02
    kind: topLevelVariable
    isNotImported: null
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_prefixed_class_constructors() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
class A {
  A.foo01();
  A.foo02();
}

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

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: constructorInvocation
    isNotImported: null
    libraryUri: null
  foo02
    kind: constructorInvocation
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_prefixed_class_getters() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
class A {
  int get foo01 => 0;
  int get foo02 => 0;
}

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

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: getter
    isNotImported: null
    libraryUri: null
  foo02
    kind: getter
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_prefixed_class_methods_instance() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
class A {
  void foo01() {}
  void foo02() {}
}

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

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
  foo02
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_prefixed_class_methods_static() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
class A {
  static void foo01() {}
  static void foo02() {}
}

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

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
  foo02
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_prefixed_expression_extensionGetters() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions(r'''
extension E1 on int {
  int get foo01 => 0;
  int get foo02 => 0;
  int get bar => 0;
}

extension E2 on double {
  int get foo03 => 0;
}

void f() {
  0.foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: getter
    isNotImported: null
    libraryUri: null
  foo02
    kind: getter
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_prefixed_expression_extensionGetters_notImported() async {
    await _configureWithWorkspaceRoot();

    newFile('$testPackageLibPath/a.dart', '''
extension E1 on int {
  int get foo01 => 0;
  int get bar => 0;
}

extension E2 on int {
  int get foo02 => 0;
}

extension E3 on double {
  int get foo03 => 0;
}
''');

    var response = await _getTestCodeSuggestions(r'''
void f() {
  0.foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: getter
    isNotImported: true
    libraryUri: package:test/a.dart
  foo02
    kind: getter
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void>
  test_prefixed_expression_extensionGetters_notImported_private() async {
    await _configureWithWorkspaceRoot();

    newFile('$testPackageLibPath/a.dart', '''
extension E1 on int {
  int get foo01 => 0;
}

extension _E2 on int {
  int get foo02 => 0;
}

extension on int {
  int get foo03 => 0;
}
''');

    var response = await _getTestCodeSuggestions(r'''
void f() {
  0.foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: getter
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_prefixed_expression_extensionMethods() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions(r'''
extension E1 on int {
  void foo01() {}
  void foo02() {}
  void bar() {}
}

extension E2 on double {
  void foo03() {}
}

void f() {
  0.foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
  foo02
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_prefixed_expression_extensionMethods_notImported() async {
    await _configureWithWorkspaceRoot();

    newFile('$testPackageLibPath/a.dart', '''
extension E1 on int {
  void foo01() {}
  void bar() {}
}

extension E2 on int {
  void foo02() {}
}

extension E3 on double {
  void foo03() {}
}
''');

    var response = await _getTestCodeSuggestions(r'''
void f() {
  0.foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: methodInvocation
    isNotImported: true
    libraryUri: package:test/a.dart
  foo02
    kind: methodInvocation
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_prefixed_expression_extensionSetters() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions(r'''
extension E1 on int {
  set foo01(int _) {}
  set foo02(int _) {}
  set bar(int _) {}
}

extension E2 on double {
  set foo03(int _) {}
}

void f() {
  0.foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: setter
    isNotImported: null
    libraryUri: null
  foo02
    kind: setter
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_prefixed_expression_extensionSetters_notImported() async {
    await _configureWithWorkspaceRoot();

    newFile('$testPackageLibPath/a.dart', '''
extension E1 on int {
  set foo01(int _) {}
  set bar(int _) {}
}

extension E2 on int {
  set foo02(int _) {}
}

extension E3 on double {
  set foo03(int _) {}
}
''');

    var response = await _getTestCodeSuggestions(r'''
void f() {
  0.foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: setter
    isNotImported: true
    libraryUri: package:test/a.dart
  foo02
    kind: setter
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void>
  test_prefixed_expression_extensionSetters_notImported_private() async {
    await _configureWithWorkspaceRoot();

    newFile('$testPackageLibPath/a.dart', '''
extension E1 on int {
  set foo01(int _) {}
}

extension _E2 on int {
  set foo02(int _) {}
}

extension on int {
  set foo03(int _) {}
}
''');

    var response = await _getTestCodeSuggestions(r'''
void f() {
  0.foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: setter
    isNotImported: true
    libraryUri: package:test/a.dart
''');
  }

  Future<void> test_prefixed_extensionGetters_imported() async {
    await _configureWithWorkspaceRoot();

    newFile('$testPackageLibPath/a.dart', '''
extension E1 on int {
  int get foo01 => 0;
  int get foo02 => 0;
  int get bar => 0;
}

extension E2 on double {
  int get foo03 => 0;
}
''');

    var response = await _getTestCodeSuggestions(r'''
import 'a.dart';

void f() {
  0.foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: getter
    isNotImported: null
    libraryUri: null
  foo02
    kind: getter
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_prefixed_extensionOverride_extensionGetters() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions(r'''
extension E1 on int {
  int get foo01 => 0;
}

extension E2 on int {
  int get foo02 => 0;
}

void f() {
  E1(0).foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: getter
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_prefixed_extensionOverride_extensionMethods() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions(r'''
extension E1 on int {
  void foo01() {}
}

extension E2 on int {
  void foo01() {}
}

void f() {
  E1(0).foo0^
}
''');

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: methodInvocation
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_prefixed_importPrefix_class() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
import 'dart:math' as math;

void f() {
  math.Rand^
}
''');

    printerConfiguration.filter = (_) => true;

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  Random
    kind: class
    isNotImported: null
    libraryUri: dart:math
  Random
    kind: constructorInvocation
    isNotImported: null
    libraryUri: dart:math
''');
  }

  Future<void> test_unprefixed_filters() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions(r'''
var foo01 = 0;
var foo02 = 0;
var bar01 = 0;
var bar02 = 0;

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

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: topLevelVariable
    isNotImported: null
    libraryUri: null
  foo02
    kind: topLevelVariable
    isNotImported: null
    libraryUri: null
''');
  }

  Future<void> test_unprefixed_imported_class() async {
    await _configureWithWorkspaceRoot();

    newFile('$testPackageLibPath/a.dart', '''
class A01 {}
''');

    newFile('$testPackageLibPath/b.dart', '''
class A02 {}
''');

    var response = await _getTestCodeSuggestions('''
import 'a.dart';
import 'b.dart';

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

    assertResponseText(response, r'''
replacement
  left: 2
suggestions
  A01
    kind: class
    isNotImported: null
    libraryUri: package:test/a.dart
  A02
    kind: class
    isNotImported: null
    libraryUri: package:test/b.dart
''');
  }

  Future<void> test_unprefixed_imported_topLevelVariable() async {
    await _configureWithWorkspaceRoot();

    newFile('$testPackageLibPath/a.dart', '''
var foo01 = 0;
''');

    newFile('$testPackageLibPath/b.dart', '''
var foo02 = 0;
''');

    var response = await _getTestCodeSuggestions('''
import 'a.dart';
import 'b.dart';

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

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo01
    kind: topLevelVariable
    isNotImported: null
    libraryUri: package:test/a.dart
  foo02
    kind: topLevelVariable
    isNotImported: null
    libraryUri: package:test/b.dart
''');
  }

  Future<void> test_unprefixed_imported_withPrefix_class() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions('''
import 'dart:math' as math;

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

    printerConfiguration.filter = (suggestion) {
      return suggestion.isClass;
    };

    // No suggestion without the `math` prefix.
    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  math.Random
    kind: class
    isNotImported: null
    libraryUri: dart:math
''');
  }

  Future<void> test_unprefixed_sorts_byScore() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions(r'''
var fooAB = 0;
var fooBB = 0;

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

    printerConfiguration
      ..filter = (suggestion) {
        return suggestion.completion.startsWith('foo');
      }
      ..sorting = printer.Sorting.asIs
      ..withRelevance = true;

    // `fooBB` has better score than `fooAB` - prefix match
    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  fooBB
    kind: topLevelVariable
    isNotImported: null
    libraryUri: null
    relevance: 503
  fooAB
    kind: topLevelVariable
    isNotImported: null
    libraryUri: null
    relevance: 503
''');
  }

  Future<void> test_unprefixed_sorts_byType() async {
    await _configureWithWorkspaceRoot();

    var response = await _getTestCodeSuggestions(r'''
var foo01 = 0.0;
var foo02 = 0;

void f() {
  int v = foo0^
}
''');

    printerConfiguration
      ..sorting = printer.Sorting.asIs
      ..withRelevance = true;

    assertResponseText(response, r'''
replacement
  left: 4
suggestions
  foo02
    kind: topLevelVariable
    isNotImported: null
    libraryUri: null
    relevance: 558
  foo01
    kind: topLevelVariable
    isNotImported: null
    libraryUri: null
    relevance: 510
''');
  }

  Future<void> test_yaml_analysisOptions_formatter() async {
    await _configureWithWorkspaceRoot();

    var path = convertPath('$testPackageRootPath/analysis_options.yaml');
    var response = await _getCodeSuggestions(
      path: path,
      content: '''
formatter:
  ^
''',
    );

    printerConfiguration
      ..filter = ((_) => true)
      ..withIsNotImported = false
      ..withLibraryUri = false;

    assertResponseText(response, r'''
suggestions
  |page_width: |
    kind: identifier
  |trailing_commas: |
    kind: identifier
''');
  }

  Future<void> test_yaml_analysisOptions_root() async {
    await _configureWithWorkspaceRoot();

    var path = convertPath('$testPackageRootPath/analysis_options.yaml');
    var response = await _getCodeSuggestions(path: path, content: '^');

    printerConfiguration
      ..filter = ((_) => true)
      ..withIsNotImported = false
      ..withLibraryUri = false;

    assertResponseText(response, r'''
suggestions
  |analyzer: |
    kind: identifier
  |code-style: |
    kind: identifier
  |formatter: |
    kind: identifier
  |include: |
    kind: identifier
  |linter: |
    kind: identifier
  |plugins: |
    kind: identifier
''');
  }

  Future<void> test_yaml_fixData_root() async {
    await _configureWithWorkspaceRoot();

    var path = convertPath('$testPackageRootPath/fix_data.yaml');
    var response = await _getCodeSuggestions(path: path, content: '^');

    printerConfiguration
      ..filter = ((_) => true)
      ..withIsNotImported = false
      ..withLibraryUri = false;

    assertResponseText(response, r'''
suggestions
  transforms:
    kind: identifier
  |version: |
    kind: identifier
''');
  }

  Future<void> test_yaml_pubspec_root() async {
    await _configureWithWorkspaceRoot();

    var path = convertPath('$testPackageRootPath/pubspec.yaml');
    var response = await _getCodeSuggestions(path: path, content: '^');

    printerConfiguration
      ..filter = ((_) => true)
      ..withIsNotImported = false
      ..withLibraryUri = false;

    assertResponseText(response, r'''
suggestions
  |dependencies: |
    kind: identifier
  |dependency_overrides: |
    kind: identifier
  |description: |
    kind: identifier
  |dev_dependencies: |
    kind: identifier
  |documentation: |
    kind: identifier
  |environment: |
    kind: identifier
  |executables: |
    kind: identifier
  |flutter: |
    kind: identifier
  |homepage: |
    kind: identifier
  |issue_tracker: |
    kind: identifier
  |name: |
    kind: identifier
  |publish_to: |
    kind: identifier
  |repository: |
    kind: identifier
  |resolution: |
    kind: identifier
  screenshots:
    kind: identifier
  |topics: |
    kind: identifier
  |version: |
    kind: identifier
  workspace:
    kind: identifier
''');
  }

  Future<void> _configureWithWorkspaceRoot() async {
    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;
  }

  Future<CompletionResponseForTesting> _getCodeSuggestions({
    required String path,
    required String content,
    int maxResults = 1 << 10,
  }) async {
    var code = TestCode.parseNormalized(content);
    var completionOffset = code.position.offset;

    newFile(path, code.code);

    return await _getSuggestions(
      path: path,
      completionOffset: completionOffset,
      maxResults: maxResults,
    );
  }

  Future<CompletionResponseForTesting> _getSuggestions({
    required String path,
    required int completionOffset,
    required int maxResults,
  }) async {
    var request = CompletionGetSuggestions2Params(
      path,
      completionOffset,
      maxResults,
    ).toRequest('0', clientUriConverter: server.uriConverter);

    var response = await handleSuccessfulRequest(request);
    var result = CompletionGetSuggestions2Result.fromResponse(
      response,
      clientUriConverter: server.uriConverter,
    );

    // Extract the internal request object.
    var dartRequest = server.completionState.currentRequest;

    return CompletionResponseForTesting(
      requestOffset: completionOffset,
      requestLocationName: dartRequest?.collectorLocationName,
      opTypeLocationName: dartRequest?.opType.completionLocation,
      replacementOffset: result.replacementOffset,
      replacementLength: result.replacementLength,
      isIncomplete: result.isIncomplete,
      suggestions: result.suggestions,
    );
  }

  Future<CompletionResponseForTesting> _getTestCodeSuggestions(
    String content, {
    int maxResults = 1 << 10,
  }) {
    return _getCodeSuggestions(
      path: convertPath(testFilePath),
      content: content,
      maxResults: maxResults,
    );
  }

  RequestWithFutureResponse _sendTestCompletionRequest(String id, int offset) {
    var request = CompletionGetSuggestions2Params(
      testFile.path,
      0,
      1 << 10,
    ).toRequest(id, clientUriConverter: server.uriConverter);
    var futureResponse = handleRequest(request);
    return RequestWithFutureResponse(offset, request, futureResponse);
  }
}

class RequestWithFutureResponse {
  final int offset;
  final Request request;
  final Future<Response> futureResponse;

  new(this.offset, this.request, this.futureResponse);

  Future<CompletionResponseForTesting> toResponse() async {
    var response = await futureResponse;
    expect(response, isResponseSuccess(request.id));
    var result = CompletionGetSuggestions2Result.fromResponse(
      response,
      clientUriConverter: null,
    );
    return CompletionResponseForTesting(
      requestOffset: offset,
      requestLocationName: null,
      opTypeLocationName: null,
      replacementOffset: result.replacementOffset,
      replacementLength: result.replacementLength,
      isIncomplete: result.isIncomplete,
      suggestions: result.suggestions,
    );
  }
}

class _SuggestionDetailsPrinter {
  final StringBuffer buffer;
  final CompletionGetSuggestionDetails2Result result;
  final ResourceProvider resourceProvider;
  final Map<File, String> fileDisplayMap;

  String _indent = '';

  new({
    required this.buffer,
    required this.result,
    required this.resourceProvider,
    required this.fileDisplayMap,
  });

  void writeResult() {
    _writelnWithIndent('completion: ${result.completion}');
    _withIndent(() {
      _writeChange(result.change);
    });
  }

  void _withIndent(void Function() f) {
    var indent = _indent;
    _indent = '$_indent  ';
    f();
    _indent = indent;
  }

  void _writeChange(SourceChange change) {
    _writelnWithIndent('change');
    _withIndent(() {
      for (var fileEdit in change.edits) {
        _writeSourceFileEdit(fileEdit);
      }
    });
  }

  void _writelnWithIndent(String line) {
    buffer.write(_indent);
    buffer.writeln(line);
  }

  void _writeSourceEdit(SourceEdit edit) {
    _writelnWithIndent('offset: ${edit.offset}');
    _writelnWithIndent('length: ${edit.length}');

    var replacementStr = edit.replacement
        .replaceAll('\n', r'\n')
        .replaceAll('\r', r'\r');
    _writelnWithIndent('replacement: $replacementStr');
  }

  void _writeSourceFileEdit(SourceFileEdit fileEdit) {
    var file = resourceProvider.getFile(fileEdit.file);
    var fileStr = fileDisplayMap[file] ?? fail('No display name: $file');
    _writelnWithIndent(fileStr);

    _withIndent(() {
      for (var edit in fileEdit.edits) {
        _writeSourceEdit(edit);
      }
    });
  }
}

extension on CompletionSuggestion {
  bool get isClass {
    return kind == CompletionSuggestionKind.IDENTIFIER &&
        element?.kind == ElementKind.CLASS;
  }
}
