// 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/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analysis_server/src/plugin/plugin_locator.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/utilities/extensions/file_system.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_testing/package_config_file_builder.dart';
import 'package:analyzer_testing/utilities/utilities.dart';
import 'package:analyzer_utilities/testing/tree_string_sink.dart';
import 'package:collection/collection.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/text_expectations.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(AnalysisDomainBlazeTest);
    defineReflectiveTests(AnalysisDomainPubTest);
    defineReflectiveTests(SetAnalysisRootsTest);
    defineReflectiveTests(SetSubscriptionsTest);
    defineReflectiveTests(UpdateTextExpectations);
  });
}

@reflectiveTest
class AnalysisDomainBlazeTest extends _AnalysisDomainTest {
  String get myPackageLibPath => '$myPackageRootPath/lib';

  String get myPackageRootPath => '$workspaceRootPath/dart/my';

  String get myPackageTestFilePath => '$myPackageLibPath/test.dart';

  @override
  void setUp() {
    super.setUp();
    newFile('$workspaceRootPath/${file_paths.blazeWorkspaceMarker}', '');
  }

  Future<void> test_fileSystem_changeFile_buildFile_legacy() async {
    // Make it a Blaze package.
    newBazelBuildFile(myPackageRootPath, r'''
# foo
''');

    newFile(myPackageTestFilePath, '''
void f(int? a) {}
''');

    await setRoots(included: [myPackageRootPath], excluded: []);
    await server.onAnalysisComplete;

    // No errors after initial analysis.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/dart/my/lib/test.dart
  errors: empty
''');

    // Change BUILD file, nothing interesting.
    newBazelBuildFile(myPackageRootPath, r'''
# bar
''');

    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // BUILD file change caused rebuilding analysis contexts.
    assertNotificationsText(r'''
AnalysisFlush
  /home/dart/my/lib/test.dart
AnalysisErrors
  file: /home/dart/my/lib/test.dart
  errors: empty
''');
  }
}

@reflectiveTest
class AnalysisDomainPubTest extends _AnalysisDomainTest {
  Future<void> test_fileSystem_addFile_analysisOptions() async {
    deleteTestPackageAnalysisOptionsFile();

    _createFilesWithErrors([
      '$testPackageLibPath/a.dart',
      '$testPackageLibPath/b.dart',
    ]);

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // Both a.dart and b.dart are analyzed.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: notEmpty
''');

    // Write the options file that excludes b.dart
    newAnalysisOptionsYamlFile(testPackageRootPath, r'''
analyzer:
  exclude:
    - lib/b.dart
''');

    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // Errors for all files were flushed, b.dart is not reported.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/lib/a.dart
  /home/test/lib/b.dart
  /home/test/pubspec.yaml
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_addFile_analysisOptions_analysis() async {
    deleteTestPackageAnalysisOptionsFile();
    _createFilesWithErrors(['$testPackageLibPath/a.dart']);

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // a.dart was analyzed
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    // Add 'analysis_options.yaml' that has an error.
    newAnalysisOptionsYamlFile(testPackageRootPath, '''
analyzer:
  error:
''');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // Both files were analyzed.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/lib/a.dart
  /home/test/pubspec.yaml
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_addFile_androidManifestXml() async {
    newAnalysisOptionsYamlFile(testPackageRootPath, '''
analyzer:
  optional-checks:
    chrome-os-manifest-checks: true
''');

    newFile('$testPackageLibPath/a.dart', '');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
''');

    newFile('$testPackageRootPath/AndroidManifest.xml', '<manifest/>');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // No touch-screen.
    // TODO(scheglov): Why duplicate analysis?
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/AndroidManifest.xml
  errors: notEmpty
AnalysisErrors
  file: /home/test/AndroidManifest.xml
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_addFile_dart() async {
    // We have to create the folder, otherwise there is nothing to watch.
    newFolder(testPackageLibPath);

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // We don't have a.dart yet.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
''');

    _createFilesWithErrors(['$testPackageLibPath/a.dart']);
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We created a.dart, so it should be analyzed.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_addFile_dart_dotFolder() async {
    var aPath = '$testPackageLibPath/.foo/a.dart';
    var bPath = '$testPackageLibPath/b.dart';

    newFile(bPath, r'''
import '.foo/a.dart';
void f(A a) {}
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // We don't have a.dart, so the import cannot be resolved.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: notEmpty
''');

    newFile(aPath, r'''
class A {}
''');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // 'a.dart' is in a dot-folder, so excluded from analysis.
    // We added a.dart with `A`, so no errors.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: empty
''');
  }

  Future<void> test_fileSystem_addFile_dart_excluded() async {
    var aPath = '$testPackageLibPath/a.dart';
    var bPath = '$testPackageLibPath/b.dart';

    newAnalysisOptionsYamlFile(testPackageRootPath, r'''
analyzer:
  exclude:
    - "**/a.dart"
''');

    newFile(bPath, r'''
import 'a.dart';
void f(A a) {}
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // We don't have a.dart, so the import cannot be resolved.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: notEmpty
''');

    newFile(aPath, r'''
class A {}
''');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We excluded 'a.dart' from analysis, no errors notification for it.
    // We added a.dart with `A`, so no errors.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: empty
''');
  }

  Future<void> test_fileSystem_addFile_fixDataFolderYaml() async {
    var path = '$testPackageLibPath/fix_data/foo.yaml';

    newFile('$testPackageLibPath/a.dart', '');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // No `fix_data.yaml` to analyze yet.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
''');

    // Create it, will be analyzed.
    newFile(path, '0: 1');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // And it has errors.
    // We don't recreate analysis contexts.
    // TODO(scheglov): Why duplicate analysis?
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/fix_data/foo.yaml
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/fix_data/foo.yaml
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_addFile_fixDataYaml() async {
    var path = '$testPackageLibPath/fix_data.yaml';

    newFile('$testPackageLibPath/a.dart', '');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // No `fix_data.yaml` to analyze yet.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
''');

    // Create it, will be analyzed.
    newFile(path, '0: 1');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // And it has errors.
    // We don't recreate analysis contexts.
    // TODO(scheglov): Why duplicate analysis?
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/fix_data.yaml
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/fix_data.yaml
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_addFile_packageConfigJsonFile() async {
    var aaaRootPath = '/packages/aaa';
    var aPath = '$aaaRootPath/lib/a.dart';

    newFile(aPath, '''
class A {}
''');

    newFile(testFilePath, '''
import 'package:aaa/a.dart';
void f(A a) {}
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // We cannot resolve `package:aaa/a.dart`
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: notEmpty
''');

    // Write `package_config.json`, recreate analysis contexts.
    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'aaa', rootFolder: getFolder(aaaRootPath)),
    );

    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We rebuilt analysis contexts.
    // We have `A` in 'package:aaa/a.dart', so no errors.
    // Errors are not reported for packages.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/analysis_options.yaml
  /home/test/lib/test.dart
  /home/test/pubspec.yaml
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: empty
''');
  }

  Future<void> test_fileSystem_addFile_pubspec_analysis() async {
    var aPath = '$testPackageLibPath/a.dart';
    var pubspecPath = '$testPackageRootPath/pubspec.yaml';

    newFile(aPath, 'error');

    // Write an empty file to force a new analysis context.
    // We look for `pubspec.yaml` files only in analysis context roots.
    newAnalysisOptionsYamlFile(testPackageRootPath, '');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // a.dart was analyzed
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    // Add a non-Dart file that we know how to analyze.
    newFile(pubspecPath, '''
name: sample
dependencies: true
''');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We rebuilt analysis contexts.
    // All files were analyzed.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/analysis_options.yaml
  /home/test/lib/a.dart
  /home/test/pubspec.yaml
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: notEmpty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_addFile_unrelated() async {
    var aPath = '$testPackageLibPath/a.dart';
    var unrelatedPath = '$testPackageRootPath/unrelated.txt';

    newFile(aPath, 'error');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // a.dart was analyzed
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    // Add an unrelated file, no analysis.
    newFile(unrelatedPath, 'anything');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // No analysis.
    assertNotificationsText(r'''
''');
  }

  Future<void> test_fileSystem_changeFile_analysisOptions() async {
    var optionsPath = '$testPackageRootPath/analysis_options.yaml';
    var aPath = '$testPackageLibPath/a.dart';
    var bPath = '$testPackageLibPath/b.dart';
    var cPath = '$testPackageLibPath/c.dart';

    _createFilesWithErrors([aPath, bPath, cPath]);

    // Exclude 'b.dart' from analysis.
    newFile(optionsPath, r'''
analyzer:
  exclude:
    - lib/b.dart
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // Only 'a.dart' and 'c.dart' are analyzed, because 'b.dart' is excluded.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/c.dart
  errors: notEmpty
''');

    // Exclude 'c.dart' from analysis.
    newFile(optionsPath, r'''
analyzer:
  exclude:
    - lib/c.dart
''');

    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // Analysis contexts were rebuilt.
    // Only 'a.dart' and 'b.dart' analyzed.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/analysis_options.yaml
  /home/test/lib/a.dart
  /home/test/lib/c.dart
  /home/test/pubspec.yaml
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_changeFile_androidManifestXml() async {
    var path = '$testPackageRootPath/AndroidManifest.xml';

    newFile('$testPackageLibPath/a.dart', '');

    // Has an error - no touch screen.
    newFile(path, '<manifest/>');

    newAnalysisOptionsYamlFile(testPackageRootPath, '''
analyzer:
  optional-checks:
    chrome-os-manifest-checks: true
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/AndroidManifest.xml
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
''');

    // Update the file, so analyze it.
    newFile(path, '<manifest/>');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // An error was reported.
    // TODO(scheglov): Why duplicate analysis?
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/AndroidManifest.xml
  errors: notEmpty
AnalysisErrors
  file: /home/test/AndroidManifest.xml
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_changeFile_dart() async {
    var aPath = '$testPackageLibPath/a.dart';
    var bPath = '$testPackageLibPath/b.dart';

    newFile(aPath, r'''
class A2 {}
''');

    newFile(bPath, r'''
import 'a.dart';
void f(A a) {}
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: notEmpty
''');

    // Update a.dart so that b.dart has no error.
    newFile(aPath, 'class A {}');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // The update of a.dart fixed the error in b.dart
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: empty
''');
  }

  Future<void> test_fileSystem_changeFile_dart_dotFolder() async {
    var aPath = '$testPackageLibPath/.foo/a.dart';
    var bPath = '$testPackageLibPath/b.dart';

    newFile(aPath, r'''
class B {}
''');

    newFile(bPath, r'''
import '.foo/a.dart';
void f(A a) {}
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // 'a.dart' is in a dot-folder, so excluded from analysis.
    // We have `B`, not `A`, in a.dart, so has errors.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: notEmpty
''');

    newFile(aPath, r'''
class A {}
''');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // 'a.dart' is in a dot-folder, so excluded from analysis.
    // We changed a.dart, to have `A`, so no errors.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: empty
''');
  }

  Future<void> test_fileSystem_changeFile_dart_excluded() async {
    var aPath = '$testPackageLibPath/a.dart';
    var bPath = '$testPackageLibPath/b.dart';

    newAnalysisOptionsYamlFile(testPackageRootPath, r'''
analyzer:
  exclude:
    - "**/a.dart"
''');

    newFile(aPath, r'''
class B {}
''');

    newFile(bPath, r'''
import 'a.dart';
void f(A a) {}
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We excluded 'a.dart' from analysis, no errors notification for it.
    // We have `B`, not `A`, in a.dart, so has errors.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: notEmpty
''');

    newFile(aPath, r'''
class A {}
''');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We changed a.dart, to have `A`, so no errors.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: empty
''');
  }

  Future<void> test_fileSystem_changeFile_fixDataFolderYaml() async {
    var path = '$testPackageLibPath/fix_data/foo.yaml';

    newFile('$testPackageLibPath/a.dart', '');

    // This file has an error.
    newFile(path, '0: 1');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // The file was analyzed.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/fix_data/foo.yaml
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
''');

    // Replace with the content that does not have errors.
    newFile(path, r'''
version: 1
transforms: []
''');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // No errors in `foo.yaml` anymore.
    // We don't recreate analysis contexts.
    // TODO(scheglov): Why duplicate analysis?
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/fix_data/foo.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/fix_data/foo.yaml
  errors: empty
''');
  }

  Future<void> test_fileSystem_changeFile_fixDataYaml() async {
    var path = '$testPackageLibPath/fix_data.yaml';

    newFile('$testPackageLibPath/a.dart', '');

    // This file has an error.
    newFile(path, '0: 1');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // The file was analyzed.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/fix_data.yaml
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
''');

    // Replace with the content that does not have errors.
    newFile(path, r'''
version: 1
transforms: []
''');
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // No errors in `fix_data.yaml` anymore.
    // We don't recreate analysis contexts.
    // TODO(scheglov): Why duplicate analysis?
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/fix_data.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/fix_data.yaml
  errors: empty
''');
  }

  Future<void> test_fileSystem_changeFile_hasOverlay_removeOverlay() async {
    newFile(testFilePath, '');

    // Add an overlay without errors.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        testFile.path: AddContentOverlay(''),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );

    await setRoots(included: [workspaceRootPath], excluded: []);

    // The test file (overlay) is analyzed, no errors.
    await _waitAnalysisComplete();
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: empty
''');

    // Change the file, has errors.
    newFile(testFilePath, 'error');

    // But the overlay is still present, so the file is not analyzed.
    await _waitAnalysisComplete();
    assertNotificationsText(r'''
''');

    // Remove the overlay, now the file will be read.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        testFile.path: RemoveContentOverlay(),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );

    // The file has errors.
    await _waitAnalysisComplete();
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_changeFile_packageConfigJsonFile() async {
    var aaaRootPath = '/packages/aaa';
    var aPath = '$aaaRootPath/lib/a.dart';

    newFile(aPath, '''
class A {}
''');

    newFile(testFilePath, '''
import 'package:aaa/a.dart';
void f(A a) {}
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // The default `package_config.json` is without `package:aaa`.
    // We cannot resolve `package:aaa/a.dart`
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: notEmpty
''');

    // Write `package_config.json`, recreate analysis contexts.
    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'aaa', rootFolder: getFolder(aaaRootPath)),
    );

    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We have `A` in 'package:aaa/a.dart', so no errors.
    // Errors are not reported for packages.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/analysis_options.yaml
  /home/test/lib/test.dart
  /home/test/pubspec.yaml
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: empty
''');
  }

  Future<void> test_fileSystem_deleteFile_analysisOptions() async {
    var optionsPath = '$testPackageRootPath/analysis_options.yaml';
    var aPath = '$testPackageLibPath/a.dart';
    var bPath = '$testPackageLibPath/b.dart';

    _createFilesWithErrors([aPath, bPath]);

    // Exclude b.dart from analysis.
    newFile(optionsPath, r'''
analyzer:
  exclude:
    - lib/b.dart
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // Only a.dart is analyzed, because b.dart is excluded.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    // Delete the options file.
    deleteFile(optionsPath);

    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // Errors for a.dart were flushed, a.dart and b.dart analyzed.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/analysis_options.yaml
  /home/test/lib/a.dart
  /home/test/pubspec.yaml
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_deleteFile_androidManifestXml() async {
    var path = '$testPackageRootPath/AndroidManifest.xml';

    newFile('$testPackageLibPath/a.dart', '');

    // Has an error - no touch screen.
    newFile(path, '<manifest/>');

    newAnalysisOptionsYamlFile(testPackageRootPath, '''
analyzer:
  optional-checks:
    chrome-os-manifest-checks: true
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // An error was reported.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/AndroidManifest.xml
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
''');

    // Delete the file.
    deleteFile(path);
    await pumpEventQueue(times: 5000);

    // We received a flush notification.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/AndroidManifest.xml
''');
  }

  /// Tests that deleting and re-creating a file while an overlay is active
  /// keeps the diagnotics when the overlay is then removed, then removes them
  /// when the file is deleted.
  ///
  /// https://github.com/dart-lang/sdk/issues/53475
  Future<void> test_fileSystem_deleteFile_createFile_withOverlay_dart() async {
    var aPath = convertPath('$testPackageLibPath/a.dart');

    _createFilesWithErrors([aPath]);

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // Initial file has errors.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    // Set the overlay, with a different content.
    // We get another notification with errors.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        aPath: AddContentOverlay('error2'),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    // Delete the file, we have the overlay, so no notifications.
    deleteFile(aPath);
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
''');

    // Re-creating the file, we have the overlay, so no notifications.
    _createFilesWithErrors([aPath]);
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
''');

    // Remove the overlay, the file has different content, so notification.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        aPath: RemoveContentOverlay(),
      }).toRequest('1', clientUriConverter: server.uriConverter),
    );
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    // Delete the file, errors are now gone.
    deleteFile(aPath);
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/lib/a.dart
''');
  }

  Future<void> test_fileSystem_deleteFile_dart() async {
    var aPath = '$testPackageLibPath/a.dart';

    _createFilesWithErrors([aPath]);

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // a.dart was analyzed
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    deleteFile(aPath);
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We deleted a.dart, its errors should be flushed.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/lib/a.dart
''');
  }

  Future<void> test_fileSystem_deleteFile_dart_excluded() async {
    var aPath = '$testPackageLibPath/a.dart';
    var bPath = '$testPackageLibPath/b.dart';

    newAnalysisOptionsYamlFile(testPackageRootPath, r'''
analyzer:
  exclude:
    - "**/a.dart"
''');

    newFile(aPath, r'''
class A {}
''');

    newFile(bPath, r'''
import 'a.dart';
void f(A a) {}
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We excluded 'a.dart' from analysis, no errors notification for it.
    // We have `A` in a.dart, so no errors.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: empty
''');

    deleteFile(aPath);
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We deleted a.dart, so `A` cannot be resolved.
    // TODO(scheglov): Should we get flush for a.dart also?
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: notEmpty
''');
  }

  Future<void> test_fileSystem_deleteFile_fixDataYaml() async {
    var path = '$testPackageLibPath/fix_data.yaml';

    newFile('$testPackageLibPath/a.dart', '');

    // This file has an error.
    newFile(path, '0: 1');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // The file was analyzed.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/fix_data.yaml
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
''');

    // Delete the file.
    deleteFile(path);
    await pumpEventQueue(times: 5000);

    // We received a flush notification.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/lib/fix_data.yaml
''');
  }

  Future<void> test_fileSystem_deleteFile_packageConfigJsonFile() async {
    var aaaRootPath = '/packages/aaa';
    var aPath = '$aaaRootPath/lib/a.dart';

    newFile(aPath, '''
class A {}
''');

    // Write the empty file, without `package:aaa`.
    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'aaa', rootFolder: getFolder(aaaRootPath)),
    );

    newFile(testFilePath, '''
import 'package:aaa/a.dart';
void f(A a) {}
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // We have `A` in 'package:aaa/a.dart', so no errors.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: empty
''');

    // Delete `package_config.json`, recreate analysis contexts.
    deleteFile('$testPackageRootPath/.dart_tool/package_config.json');

    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;

    // We cannot resolve 'package:aaa/a.dart', so errors.
    // Errors are not reported for packages.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/analysis_options.yaml
  /home/test/lib/test.dart
  /home/test/pubspec.yaml
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: notEmpty
''');
  }

  /// Tests that deleting a file does not clear diagnostics if there's still
  /// an active overlay for the file.
  ///
  /// https://github.com/dart-lang/sdk/issues/53475
  Future<void> test_fileSystem_deleteFile_withOverlay_dart() async {
    var aPath = convertPath('$testPackageLibPath/a.dart');

    _createFilesWithErrors([aPath]);

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // Initial file has errors.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    // Set overlay with different content, with errors.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        aPath: AddContentOverlay('error2'),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    // Delete file, has the overlay, no notifications.
    deleteFile(aPath);
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
''');

    // After removing the overlay, errors are gone.
    // TODO(scheglov): why not flush?
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        aPath: RemoveContentOverlay(),
      }).toRequest('1', clientUriConverter: server.uriConverter),
    );
    await pumpEventQueue(times: 5000);
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
''');
  }

  Future<void> test_setPriorityFiles() async {
    var a = getFile('$workspaceRootPath/foo/lib/a.dart');
    var b = getFile('$workspaceRootPath/foo/lib/b.dart');
    var c = getFile('$workspaceRootPath/bar/lib/c.dart');
    var d = getFile('$workspaceRootPath/bar/lib/d.dart');

    a.writeAsStringSync('');
    b.writeAsStringSync('');
    c.writeAsStringSync('');
    d.writeAsStringSync('');

    await handleSuccessfulRequest(
      AnalysisSetPriorityFilesParams([
        a.path,
        c.path,
      ]).toRequest('0', clientUriConverter: server.uriConverter),
    );

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // a.dart and c.dart are priority files.
    // So, they are analyzed before b.dart and d.dart
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/foo/lib/a.dart
  errors: empty
AnalysisErrors
  file: /home/bar/lib/c.dart
  errors: empty
AnalysisErrors
  file: /home/foo/lib/b.dart
  errors: empty
AnalysisErrors
  file: /home/bar/lib/d.dart
  errors: empty
''');
  }

  Future<void> test_setPriorityFiles_notAbsolute() async {
    var response = await handleRequest(
      AnalysisSetPriorityFilesParams([
        'a.dart',
      ]).toRequest('0', clientUriConverter: server.uriConverter),
    );

    expect(
      response,
      isResponseFailure('0', RequestErrorCode.INVALID_FILE_PATH_FORMAT),
    );
  }

  Future<void> test_setPriorityFiles_withoutRoots() async {
    await handleSuccessfulRequest(
      AnalysisSetPriorityFilesParams([
        convertPath('$testPackageLibPath/a.dart'),
      ]).toRequest('0', clientUriConverter: server.uriConverter),
    );
  }

  Future<void> test_setRoots_excluded_notAbsolute() async {
    var response = await handleRequest(
      AnalysisSetAnalysisRootsParams(
        [workspaceRootPath],
        ['foo'],
        packageRoots: {},
      ).toRequest('0', clientUriConverter: server.uriConverter),
    );

    expect(
      response,
      isResponseFailure('0', RequestErrorCode.INVALID_FILE_PATH_FORMAT),
    );
  }

  Future<void> test_setRoots_excluded_notNormalized() async {
    var response = await handleRequest(
      AnalysisSetAnalysisRootsParams(
        [workspaceRootPath],
        [convertPath('/foo/../bar')],
        packageRoots: {},
      ).toRequest('0', clientUriConverter: server.uriConverter),
    );

    expect(
      response,
      isResponseFailure('0', RequestErrorCode.INVALID_FILE_PATH_FORMAT),
    );
  }

  Future<void> test_setRoots_included_notAbsolute() async {
    var response = await handleRequest(
      AnalysisSetAnalysisRootsParams(
        ['foo'],
        [],
        packageRoots: {},
      ).toRequest('0', clientUriConverter: server.uriConverter),
    );

    expect(
      response,
      isResponseFailure('0', RequestErrorCode.INVALID_FILE_PATH_FORMAT),
    );
  }

  Future<void> test_setRoots_included_notNormalized() async {
    var response = await handleRequest(
      AnalysisSetAnalysisRootsParams(
        [convertPath('/foo/../bar')],
        [],
        packageRoots: {},
      ).toRequest('0', clientUriConverter: server.uriConverter),
    );

    expect(
      response,
      isResponseFailure('0', RequestErrorCode.INVALID_FILE_PATH_FORMAT),
    );
  }

  Future<void> test_setRoots_includedFile() async {
    var aPath = '$testPackageLibPath/a.dart';
    var bPath = '$testPackageLibPath/b.dart';

    _createFilesWithErrors([aPath, bPath]);

    await setRoots(included: [aPath], excluded: []);

    // Only a.dart is included, so b.dart is not analyzed.
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');
  }

  Future<void> test_setRoots_includedFile_setRoots() async {
    var aPath = '$testPackageLibPath/a.dart';
    var bPath = '$testPackageLibPath/b.dart';

    _createFilesWithErrors([aPath, bPath]);

    // Include only single file.
    await setRoots(included: [aPath], excluded: []);
    await server.onAnalysisComplete;

    // So, only a.dart is analyzed, and b.dart is not.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');

    // Include the folder that contains both a.dart and b.dart
    await setRoots(included: [testPackageRootPath], excluded: []);
    await server.onAnalysisComplete;

    // So, both a.dart and b.dart are analyzed.
    assertNotificationsText(r'''
AnalysisFlush
  /home/test/lib/a.dart
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/b.dart
  errors: notEmpty
''');
  }

  Future<void> test_setRoots_includedFileFolder() async {
    var includedFile = '$testPackageLibPath/a.dart';
    var includedFolder = '$testPackageLibPath/foo';
    var includedFolderFile1 = '$includedFolder/1.dart';
    var includedFolderFile2 = '$includedFolder/2.dart';
    var notIncludedFile = '$testPackageLibPath/b.dart';

    _createFilesWithErrors([
      includedFile,
      includedFolderFile1,
      includedFolderFile2,
      notIncludedFile,
    ]);

    await setRoots(included: [includedFile, includedFolder], excluded: []);
    await server.onAnalysisComplete;

    // We can combine a file, and a folder as included paths.
    // And the file that is not in there is not analyzed.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/foo/1.dart
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/foo/2.dart
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');
  }

  Future<void> test_setRoots_includedFolder_analysisOptions_exclude() async {
    var aPath = '$testPackageLibPath/a.dart';
    var bPath = '$testPackageLibPath/b.dart';

    newAnalysisOptionsYamlFile(testPackageRootPath, '''
analyzer:
  exclude:
    - "**/b.dart"
''');

    _createFilesWithErrors([aPath, bPath]);

    await setRoots(included: [workspaceRootPath], excluded: []);

    // b.dart is excluded using the options file.
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');
  }

  @FailingTest(reason: 'Not implemented in ContextLocator')
  Future<void> test_setRoots_includedFolder_excludedFile() async {
    var aPath = '$testPackageLibPath/a.dart';
    var excluded_path = '$testPackageRootPath/excluded/b.dart';

    _createFilesWithErrors([aPath, excluded_path]);

    await setRoots(included: [workspaceRootPath], excluded: [excluded_path]);
    await server.onAnalysisComplete;

    fail('Not implemented');
  }

  Future<void> test_setRoots_includedFolder_excludedFolder() async {
    var aPath = '$testPackageLibPath/a.dart';
    var excluded_path = '$testPackageRootPath/excluded/b.dart';

    _createFilesWithErrors([aPath, excluded_path]);

    await setRoots(
      included: [workspaceRootPath],
      excluded: ['$testPackageRootPath/excluded'],
    );
    await server.onAnalysisComplete;

    // a.dart is analyzed, but b.dart is in the excluded folder.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');
  }

  Future<void> test_setRoots_includedFolder_notExisting() async {
    var existingFolder_path = '$testPackageLibPath/exiting';
    var notExistingFolder_path = '$testPackageLibPath/notExisting';
    var existingFile_path = '$existingFolder_path/1.dart';

    _createFilesWithErrors([existingFile_path]);

    await setRoots(
      included: [existingFolder_path, notExistingFolder_path],
      excluded: [],
    );
    await server.onAnalysisComplete;

    // The not existing root does not prevent analysis of the existing one.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/exiting/1.dart
  errors: notEmpty
''');
  }

  Future<void> test_setRoots_notDartFile_analysisOptions_excluded() async {
    deleteTestPackageAnalysisOptionsFile();
    var aPath = '$testPackageLibPath/a.dart';
    var optionsPath = '$testPackageRootPath/analysis_options.yaml';

    newFile(aPath, 'error');

    // 'analysis_options.yaml' that has an error and excludes itself.
    newFile(optionsPath, '''
analyzer:
  exclude:
    - analysis_options.yaml
  error:
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');
  }

  Future<void> test_setRoots_notDartFile_androidManifestXml() async {
    var path = '$testPackageRootPath/AndroidManifest.xml';

    newFile('$testPackageLibPath/a.dart', '');

    newAnalysisOptionsYamlFile(testPackageRootPath, '''
analyzer:
  optional-checks:
    chrome-os-manifest-checks: true
''');

    newFile(path, '<manifest/>');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();

    // No touch-screen.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/AndroidManifest.xml
  errors: notEmpty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: empty
''');
  }

  Future<void> test_setRoots_notDartFile_fixDataYaml() async {
    var path = '$testPackageLibPath/fix_data.yaml';

    // `lib/fix_data.yaml` will be analyzed.
    newFile(path, '0: 1');

    await setRoots(included: [workspaceRootPath], excluded: []);

    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/fix_data.yaml
  errors: notEmpty
''');
  }

  Future<void> test_setRoots_notDartFile_fixDataYaml_empty() async {
    var path = '$testPackageLibPath/fix_data.yaml';
    newFile(path, '');

    await setRoots(included: [workspaceRootPath], excluded: []);

    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/fix_data.yaml
  errors: empty
''');
  }

  Future<void> test_setRoots_notDartFile_fixDataYaml_onlyComments() async {
    var path = '$testPackageLibPath/fix_data.yaml';
    newFile(path, '# one\n#two');

    await setRoots(included: [workspaceRootPath], excluded: []);

    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/fix_data.yaml
  errors: empty
''');
  }

  Future<void> test_setRoots_notDartFile_pubspec_excluded() async {
    deleteTestPackageAnalysisOptionsFile();
    var aPath = '$testPackageLibPath/a.dart';
    var optionsPath = '$testPackageRootPath/analysis_options.yaml';

    newFile(aPath, 'error');

    writeTestPackagePubspecYamlFile('''
name:
  - error
''');

    // 'analysis_options.yaml' that excludes pubspec.yaml.
    newFile(optionsPath, '''
analyzer:
  exclude:
    - pubspec.yaml
''');

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // No pubspec.yaml
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/a.dart
  errors: notEmpty
''');
  }

  Future<void> test_setRoots_packageConfigJsonFile() async {
    var aaaRootPath = '/packages/aaa';
    var aPath = '$aaaRootPath/lib/a.dart';

    newFile(aPath, '''
class A {}
''');

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

    newFile(testFilePath, '''
import 'package:aaa/a.dart';
void f(A a) {}
''');

    // create project and wait for analysis
    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    // We have `A` in 'package:aaa/a.dart', so no errors.
    // Errors are not reported for packages.
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: empty
''');
  }

  Future<void> test_updateContent_addOverlay() async {
    newFile(testFilePath, 'error');

    await setRoots(included: [workspaceRootPath], excluded: []);

    // The file in the file system has errors.
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: notEmpty
''');

    // Add an overlay without errors.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        testFile.path: AddContentOverlay(''),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );

    // A new errors notification was received, no errors.
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: empty
''');
  }

  Future<void> test_updateContent_changeOverlay() async {
    newFile(testFilePath, '');

    // Add the content with an error.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        testFile.path: AddContentOverlay('var v = 0'),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );

    await setRoots(included: [workspaceRootPath], excluded: []);

    // The overlay has an error.
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/pubspec.yaml
  errors: empty
AnalysisErrors
  file: /home/test/analysis_options.yaml
  errors: empty
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: notEmpty
''');

    // Add the missing `;`.
    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        testFile.path: ChangeContentOverlay([SourceEdit(9, 0, ';')]),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );

    // A new errors notification was received, no errors.
    await server.onAnalysisComplete;
    assertNotificationsText(r'''
AnalysisErrors
  file: /home/test/lib/test.dart
  errors: empty
''');
  }

  Future<void> test_updateContent_notAbsolute() async {
    var response = await handleRequest(
      AnalysisUpdateContentParams({
        'a.dart': AddContentOverlay(''),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );
    expect(response, isResponseFailure('0'));
  }

  Future<void> test_updateContent_outOfRange_beyondEnd() {
    return _updateContent_outOfRange('012', SourceEdit(0, 5, 'foo'));
  }

  Future<void> test_updateContent_outOfRange_negativeLength() {
    return _updateContent_outOfRange('', SourceEdit(3, -1, 'foo'));
  }

  Future<void> test_updateContent_outOfRange_negativeOffset() {
    return _updateContent_outOfRange('', SourceEdit(-1, 3, 'foo'));
  }

  Future<void> _updateContent_outOfRange(
    String initialContent,
    SourceEdit edit,
  ) async {
    newFile(testFilePath, initialContent);

    await setRoots(included: [workspaceRootPath], excluded: []);
    await server.onAnalysisComplete;

    await handleSuccessfulRequest(
      AnalysisUpdateContentParams({
        testFile.path: AddContentOverlay(initialContent),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );

    var response = await handleRequest(
      AnalysisUpdateContentParams({
        testFile.path: ChangeContentOverlay([edit]),
      }).toRequest('0', clientUriConverter: server.uriConverter),
    );

    expect(
      response,
      isResponseFailure('0', RequestErrorCode.INVALID_OVERLAY_CHANGE),
    );
  }

  /// Pump the event queue, so that watch events are processed.
  /// Wait for analysis to complete.
  /// Repeat a few times, eventually there will be no work to do.
  Future<void> _waitAnalysisComplete() async {
    for (var i = 0; i < 128; i++) {
      await pumpEventQueue(times: 5000);
      await server.onAnalysisComplete;
    }
  }
}

@reflectiveTest
class SetAnalysisRootsTest extends PubPackageAnalysisServerTest {
  /// Verifies the set of context roots created by the server and which were
  /// provided to each plugin.
  void expectPluginMapping(Map<String, List<String>> expected) {
    var pluginMapping = pluginManager.contextRootPlugins.map(
      (root, plugins) => MapEntry(
        pathContext.basename(root.root.path),
        plugins.map(
          (pluginPath) => pathContext.basename(
            pathContext.dirname(pathContext.dirname(pluginPath)),
          ),
        ),
      ),
    );

    // Additionally, add any context roots from the server that were not
    // provided to plugins so the tests can also explicitly verify roots were
    // created that didn't show up in the plugins list.
    for (var serverContext in server.contextManager.analysisContexts) {
      var name = pathContext.basename(serverContext.contextRoot.root.path);
      pluginMapping.putIfAbsent(name, () => []);
    }

    expect(pluginMapping, expected);
  }

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

    // These tests don't use the "test" package folder but have their own named
    // package folders. Delete the "test" folder so it doesn't show up as a
    // context root (else it would need listing in each test expectation).
    deleteFolder(testPackageRootPath);
  }

  /// Tests a package with a nested folder that has an additional
  /// `analysis_options.yaml` that does not enable the plugin. An additional
  /// context should be created for the non-plugin folder and it should be
  /// excluded from the parents plugin root.
  Future<void>
  test_sentToPlugins_inNestedPackages_withNestedAnalysisOptions_enabledPlugin_disabledPlugin() async {
    var plugin1 = (name: 'plugin1', path: _createPlugin('plugin1'));

    // package1 has plugin2 enabled.
    _createTestPackage(
      'package1',
      withPackageConfig: false,
      plugins: [plugin1],
    );

    // nestedFolder1 has no plugins enabled.
    newAnalysisOptionsYamlFile(
      join(workspaceRootPath, 'package1', 'nestedFolder1'),
      analysisOptionsContent(experiments: experiments),
    );

    // Write the single package config at the root that can resolve both
    // plugins.
    newPackageConfigJsonFileFromBuilder(
      workspaceRootPath,
      PackageConfigFileBuilder()
        ..add(name: 'plugin1', rootFolder: getFolder(plugin1.path)),
    );

    // Set the analysis roots to the folder ('/home') that contains both
    // packages but not the plugins (which are in '/plugins').
    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();

    expectPluginMapping({
      'home': [],
      'package1': ['plugin1'],
      'nestedFolder1': [],
    });
  }

  /// Tests a package with a nested folder that has an additional
  /// `analysis_options.yaml` that enables a different plugin. An additional
  /// root should be created.
  Future<void>
  test_sentToPlugins_inNestedPackages_withNestedAnalysisOptions_enabledPlugin_enabledDifferentPlugin() async {
    var plugin1 = (name: 'plugin1', path: _createPlugin('plugin1'));
    var plugin2 = (name: 'plugin2', path: _createPlugin('plugin2'));

    // package1 has plugin2 enabled.
    _createTestPackage(
      'package1',
      withPackageConfig: false,
      plugins: [plugin1],
    );

    // nestedFolder1 has plugin2 enabled.
    newAnalysisOptionsYamlFile(
      join(workspaceRootPath, 'package1', 'nestedFolder1'),
      analysisOptionsContent(
        experiments: experiments,
        legacyPlugins: [plugin2.name],
      ),
    );

    // Write the single package config at the root that can resolve both
    // plugins.
    newPackageConfigJsonFileFromBuilder(
      workspaceRootPath,
      PackageConfigFileBuilder()
        ..add(name: 'plugin1', rootFolder: getFolder(plugin1.path))
        ..add(name: 'plugin2', rootFolder: getFolder(plugin2.path)),
    );

    // Set the analysis roots to the folder ('/home') that contains both
    // packages but not the plugins (which are in '/plugins').
    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();

    expectPluginMapping({
      'home': [],
      'package1': ['plugin1'],
      'nestedFolder1': ['plugin2'],
    });
  }

  /// Tests a package with a nested folder that has an additional
  /// `analysis_options.yaml` that enables the same plugin explicitly. No
  /// additional context needs to be created.
  Future<void>
  test_sentToPlugins_inNestedPackages_withNestedAnalysisOptions_enabledPlugin_enabledPluginExplicit() async {
    var plugin1 = (name: 'plugin1', path: _createPlugin('plugin1'));

    // package1 has plugin2 enabled.
    _createTestPackage(
      'package1',
      withPackageConfig: false,
      plugins: [plugin1],
    );

    // nestedFolder1 also has plugin1 enabled.
    newAnalysisOptionsYamlFile(
      join(workspaceRootPath, 'package1', 'nestedFolder1'),
      analysisOptionsContent(
        experiments: experiments,
        legacyPlugins: [plugin1.name],
      ),
    );

    // Write the single package config at the root that can resolve both
    // plugins.
    newPackageConfigJsonFileFromBuilder(
      workspaceRootPath,
      PackageConfigFileBuilder()
        ..add(name: 'plugin1', rootFolder: getFolder(plugin1.path)),
    );

    // Set the analysis roots to the folder ('/home') that contains both
    // packages but not the plugins (which are in '/plugins').
    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();

    expectPluginMapping({
      'home': [],
      'package1': ['plugin1'],
      // nestedFolder1 is included as part of package1.
    });
  }

  /// Tests a package with a nested folder that has an additional
  /// `analysis_options.yaml` that enables the same plugin by including the
  /// parents `analysis_options.yaml`. No additional context needs to be
  /// created.
  Future<void>
  test_sentToPlugins_inNestedPackages_withNestedAnalysisOptions_enabledPlugin_enabledPluginInclude() async {
    var plugin1 = (name: 'plugin1', path: _createPlugin('plugin1'));

    // package1 has plugin2 enabled.
    _createTestPackage(
      'package1',
      withPackageConfig: false,
      plugins: [plugin1],
    );

    // nestedFolder1 also has plugin1 enabled because it includes the parent
    // `analysis_options.yaml`.
    newAnalysisOptionsYamlFile(
      join(workspaceRootPath, 'package1', 'nestedFolder1'),
      analysisOptionsContent(includes: ['../analysis_options.yaml']),
    );

    // Write the single package config at the root that can resolve both
    // plugins.
    newPackageConfigJsonFileFromBuilder(
      workspaceRootPath,
      PackageConfigFileBuilder()
        ..add(name: 'plugin1', rootFolder: getFolder(plugin1.path)),
    );

    // Set the analysis roots to the folder ('/home') that contains both
    // packages but not the plugins (which are in '/plugins').
    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();

    expectPluginMapping({
      'home': [],
      'package1': ['plugin1'],
      // nestedFolder1 is included as part of package1.
    });
  }

  /// Test that the correct context roots are passed to plugins when they are
  /// enabled only for projects nested within the workspace (which do not have
  /// their own package configs but use the one from the root).
  ///
  /// In this example, we have three nested projects that use a mix of plugins:
  ///
  /// - root/ (no plugins enabled)
  ///   - package1/ (plugin1, (plugin2 - disabled due to limit))
  ///   - package2/ (plugin2, (plugin1 - disabled due to limit))
  ///   - package3/ (plugin1)
  Future<void>
  test_sentToPlugins_inNestedPackages_withoutPackageConfigs() async {
    var plugin1 = (name: 'plugin1', path: _createPlugin('plugin1'));
    var plugin2 = (name: 'plugin2', path: _createPlugin('plugin2'));

    // Only the first plugin for each will be enabled due to the 1-plugin-limit.
    _createTestPackage(
      'package1',
      withPackageConfig: false,
      plugins: [plugin1, plugin2],
    );
    _createTestPackage(
      'package2',
      withPackageConfig: false,
      plugins: [plugin2, plugin1],
    );
    _createTestPackage(
      'package3',
      withPackageConfig: false,
      plugins: [plugin1],
    );

    // Write the single package config at the root that can resolve both
    // plugins.
    newPackageConfigJsonFileFromBuilder(
      workspaceRootPath,
      PackageConfigFileBuilder()
        ..add(name: 'plugin1', rootFolder: getFolder(plugin1.path))
        ..add(name: 'plugin2', rootFolder: getFolder(plugin2.path)),
    );

    // Set the analysis roots to the folder ('/home') that contains both
    // packages but not the plugins (which are in '/plugins').
    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();

    expectPluginMapping({
      'home': [],
      'package1': ['plugin1'],
      'package2': ['plugin2'],
      'package3': ['plugin1'],
    });
  }

  /// Test that the correct context roots are passed to plugins when they are
  /// enabled only for projects nested within the workspace (which have their
  /// own package configs).
  ///
  /// In this example, we have three nested projects that use a mix of plugins:
  ///
  /// - root/ (no plugins enabled)
  ///   - package1/ (plugin1, (plugin2 - disabled due to limit))
  ///   - package2/ (plugin2, (plugin1 - disabled due to limit))
  ///   - package3/ (plugin1)
  Future<void> test_sentToPlugins_inNestedPackages_withPackageConfigs() async {
    var plugin1 = (name: 'plugin1', path: _createPlugin('plugin1'));
    var plugin2 = (name: 'plugin2', path: _createPlugin('plugin2'));

    // Only the first plugin for each will be enabled due to the 1-plugin-limit.
    _createTestPackage('package1', plugins: [plugin1, plugin2]);
    _createTestPackage('package2', plugins: [plugin2, plugin1]);
    _createTestPackage('package3', plugins: [plugin1]);

    // Ensure the root directory can resolve both plugin packages even though
    // we don't enabled them for the root. This will catch if we incorrectly try
    // to enable them because we read a child analysis_options.
    newPackageConfigJsonFileFromBuilder(
      workspaceRootPath,
      PackageConfigFileBuilder()
        ..add(name: 'plugin1', rootFolder: getFolder(plugin1.path))
        ..add(name: 'plugin2', rootFolder: getFolder(plugin2.path)),
    );

    // Set the analysis roots to the folder ('/home') that contains both
    // packages but not the plugins (which are in '/plugins').
    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();

    expectPluginMapping({
      'home': [],
      'package1': ['plugin1'],
      'package2': ['plugin2'],
      'package3': ['plugin1'],
    });
  }

  /// Test that the correct context roots are passed to plugins when they are
  /// enabled for projects that contain subfolders that do not contain analysis
  /// options and so the existing plugins apply.
  ///
  /// In this example, a project has plugins and it has subfolders that inherit
  /// those options (and thus do not produce a new root).
  ///
  /// - root/ (no plugins enabled)
  ///   - package1/ (plugin1)
  ///   - package1/lib/ (no explicit options, plugin1 implied)
  Future<void> test_sentToPlugins_inNestedPackages_withSubFolders() async {
    var plugin1 = (name: 'plugin1', path: _createPlugin('plugin1'));

    // Only the first plugin for each will be enabled due to the 1-plugin-limit.
    var package1Path = _createTestPackage(
      'package1',
      withPackageConfig: false,
      plugins: [plugin1],
    );
    newFolder(join(package1Path, 'lib'));

    // Write the single package config at the root that can resolve the plugin.
    newPackageConfigJsonFileFromBuilder(
      workspaceRootPath,
      PackageConfigFileBuilder()
        ..add(name: 'plugin1', rootFolder: getFolder(plugin1.path)),
    );

    // Set the analysis roots to the folder ('/home') that contains the
    // package but not the plugin (which is in '/plugins').
    await setRoots(included: [workspaceRootPath], excluded: []);
    await waitForTasksFinished();

    expectPluginMapping({
      'home': [],
      'package1': ['plugin1'],
    });
  }

  /// Creates a plugin package named [name] and returns the path to the root
  /// of the package.
  String _createPlugin(String name) {
    var pluginDirectory = convertPath(join('/plugins', name));
    newPubspecYamlFile(pluginDirectory, 'name: $name');
    newFile(convertPath(join(pluginDirectory, 'lib', '$name.dart')), '');
    newFolder(
      join(
        pluginDirectory,
        PluginLocator.toolsFolderName,
        PluginLocator.defaultPluginFolderName,
        '$name.dart',
      ),
    );
    return pluginDirectory;
  }

  /// Creates a package in the test workspace named [name] that uses [plugins].
  String _createTestPackage(
    String name, {
    bool withPackageConfig = true,
    List<({String name, String path})> plugins = const [],
  }) {
    var packagePath = join(workspaceRootPath, name);

    newPubspecYamlFile(packagePath, 'name: $name');

    newAnalysisOptionsYamlFile(
      packagePath,
      analysisOptionsContent(
        experiments: experiments,
        legacyPlugins: plugins.map((plugin) => plugin.name).toList(),
      ),
    );

    if (withPackageConfig) {
      var packageConfig = PackageConfigFileBuilder()
        ..add(name: name, rootFolder: getFolder(packagePath));
      for (var plugin in plugins) {
        packageConfig.add(
          name: plugin.name,
          rootFolder: getFolder(plugin.path),
        );
      }
      newPackageConfigJsonFileFromBuilder(packagePath, packageConfig);
    }

    return packagePath;
  }
}

@reflectiveTest
class SetSubscriptionsTest extends PubPackageAnalysisServerTest {
  Map<File, List<HighlightRegion>> filesHighlights = {};

  final Completer<void> _resultsAvailable = Completer();

  @override
  void processNotification(Notification notification) {
    super.processNotification(notification);
    if (notification.event == analysisNotificationHighlights) {
      var params = AnalysisHighlightsParams.fromNotification(
        notification,
        clientUriConverter: server.uriConverter,
      );
      filesHighlights[getFile(params.file)] = params.regions;
      _resultsAvailable.complete();
    }
  }

  @override
  Future<void> setUp() async {
    super.setUp();
    await setRoots(included: [workspaceRootPath], excluded: []);
  }

  Future<void> test_afterAnalysis() async {
    addTestFile('int V = 42;');
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[testFile], isNull);
    // subscribe
    await addAnalysisSubscription(AnalysisService.HIGHLIGHTS, testFile);
    await _resultsAvailable.future;
    // there are results
    expect(filesHighlights[testFile], isNotEmpty);
  }

  Future<void> test_afterAnalysis_noSuchFile() async {
    var file = getFile('/no-such-file.dart');
    addTestFile('// no matter');
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[testFile], isNull);
    // subscribe
    await addAnalysisSubscription(AnalysisService.HIGHLIGHTS, file);
    await _resultsAvailable.future;
    // there are results
    expect(filesHighlights[file], isEmpty);
  }

  Future<void> test_afterAnalysis_packageFile_external() async {
    var pkgFile = newFile('/packages/pkgA/lib/libA.dart', '''
library lib_a;
class A {}
''');
    writePackageConfig(
      convertPath('/project'),
      config: (PackageConfigFileBuilder()
        ..add(name: 'pkgA', rootFolder: getFolder('/packages/pkgA'))),
    );
    //
    addTestFile('''
import 'package:pkgA/libA.dart';
void f() {
  new A();
}
''');
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[pkgFile], isNull);
    // subscribe
    await addAnalysisSubscription(AnalysisService.HIGHLIGHTS, pkgFile);
    await _resultsAvailable.future;
    // there are results
    expect(filesHighlights[pkgFile], isNotEmpty);
  }

  Future<void> test_afterAnalysis_packageFile_inRoot() async {
    var pkgA = convertPath('/pkgA');
    var pkgB = convertPath('/pkgA');
    var pkgFileA = newFile('$pkgA/lib/libA.dart', '''
library lib_a;
class A {}
''');
    newFile('$pkgA/lib/libB.dart', '''
import 'package:pkgA/libA.dart';
void f() {
  new A();
}
''');
    // add 'pkgA' and 'pkgB' as projects
    await setRoots(included: [pkgA, pkgB], excluded: []);
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[pkgFileA], isNull);
    // subscribe
    await addAnalysisSubscription(AnalysisService.HIGHLIGHTS, pkgFileA);
    await _resultsAvailable.future;
    // there are results
    expect(filesHighlights[pkgFileA], isNotEmpty);
  }

  Future<void> test_afterAnalysis_packageFile_notUsed() async {
    var pkgFile = newFile('/packages/pkgA/lib/libA.dart', '''
library lib_a;
class A {}
''');
    writePackageConfig(
      convertPath('/project'),
      config: (PackageConfigFileBuilder()
        ..add(name: 'pkgA', rootFolder: getFolder('/packages/pkgA'))),
    );
    //
    addTestFile('// no "pkgA" reference');
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[pkgFile], isNull);
    // make it a priority file, so make analyzable
    server.setPriorityFiles('0', [pkgFile.path]);
    // subscribe
    await addAnalysisSubscription(AnalysisService.HIGHLIGHTS, pkgFile);
    await _resultsAvailable.future;
    // there are results
    expect(filesHighlights[pkgFile], isNotEmpty);
  }

  Future<void> test_afterAnalysis_sdkFile() async {
    var file = getFile('/sdk/lib/core/core.dart');
    addTestFile('// no matter');
    // wait for analysis, no results initially
    await waitForTasksFinished();
    expect(filesHighlights[file], isNull);
    // subscribe
    await addAnalysisSubscription(AnalysisService.HIGHLIGHTS, file);
    await _resultsAvailable.future;
    // there are results
    expect(filesHighlights[file], isNotEmpty);
  }

  Future<void> test_beforeAnalysis() async {
    addTestFile('int V = 42;');
    // subscribe
    await addAnalysisSubscription(AnalysisService.HIGHLIGHTS, testFile);
    // wait for analysis
    await waitForTasksFinished();
    expect(filesHighlights[testFile], isNotEmpty);
  }

  Future<void> test_sentToPlugins() async {
    addTestFile('int V = 42;');
    // subscribe
    await addAnalysisSubscription(AnalysisService.HIGHLIGHTS, testFile);
    // wait for analysis
    await waitForTasksFinished();
    var params = pluginManager.analysisSetSubscriptionsParams!;
    var subscriptions = params.subscriptions;
    expect(subscriptions, hasLength(1));
    var files = subscriptions[plugin.AnalysisService.HIGHLIGHTS];
    expect(files, [testFile.path]);
  }
}

class _AnalysisDomainTest extends PubPackageAnalysisServerTest {
  final configuration = _NotificationPrinterConfiguration();
  final List<(String, Object)> notifications = [];

  void assertNotificationsText(String expected) {
    var buffer = StringBuffer();
    var sink = TreeStringSink(sink: buffer, indent: '');
    _NotificationPrinter(
      configuration: configuration,
      resourceProvider: resourceProvider,
      sink: sink,
    ).writeNotifications(notifications);
    notifications.clear();

    var actual = buffer.toString();
    if (actual != expected) {
      print('-------- Actual --------');
      print('$actual------------------------');
      TextExpectationsCollector.add(actual);
    }
    expect(actual, expected);
  }

  @override
  void processNotification(Notification notification) {
    if (notification.event == analysisNotificationFlushResults) {
      var decoded = AnalysisFlushResultsParams.fromNotification(
        notification,
        clientUriConverter: server.uriConverter,
      );
      notifications.add((notification.event, decoded));
    }
    if (notification.event == analysisNotificationErrors) {
      var decoded = AnalysisErrorsParams.fromNotification(
        notification,
        clientUriConverter: server.uriConverter,
      );
      notifications.add((notification.event, decoded));
    }
  }

  /// Create files with a content that has a compile time error.
  void _createFilesWithErrors(List<String> paths) {
    for (var path in paths) {
      newFile(path, 'error');
    }
  }
}

class _NotificationPrinter {
  final _NotificationPrinterConfiguration configuration;
  final ResourceProvider resourceProvider;
  final TreeStringSink sink;

  new({
    required this.configuration,
    required this.resourceProvider,
    required this.sink,
  });

  void writeNotifications(List<(String, Object)> notifications) {
    for (var entry in notifications) {
      _writeNotification(entry.$1, entry.$2);
    }
  }

  void _writelnFile(String path, {String? name}) {
    sink.writeIndentedLine(() {
      if (name != null) {
        sink.write('$name: ');
      }
      var file = resourceProvider.getFile(path);
      sink.write(file.posixPath);
    });
  }

  void _writeNotification(String event, Object notification) {
    switch (notification) {
      case AnalysisFlushResultsParams():
        var files = notification.files.sorted();
        sink.writeElements('AnalysisFlush', files, _writelnFile);
      case AnalysisErrorsParams():
        sink.writelnWithIndent('AnalysisErrors');
        sink.withIndent(() {
          _writelnFile(name: 'file', notification.file);
          if (configuration.withAnalysisErrorDetails) {
            throw UnimplementedError();
          } else if (notification.errors.isNotEmpty) {
            sink.writelnWithIndent('errors: notEmpty');
          } else {
            sink.writelnWithIndent('errors: empty');
          }
        });
      default:
        throw UnimplementedError('${notification.runtimeType}');
    }
  }
}

class _NotificationPrinterConfiguration {
  bool withAnalysisErrorDetails = false;
}
