// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/server_capabilities_computer.dart';
import 'package:analysis_server/src/plugin/plugin_isolate.dart';
import 'package:analysis_server/src/scheduler/message_scheduler.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:language_server_protocol/json_parsing.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import 'change_workspace_folders_test.dart';
import 'request_helpers_mixin.dart';
import 'server_abstract.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(InitializationTest);
    defineReflectiveTests(StringIdInitializationTest);
    defineReflectiveTests(SlowInitializationTest);
  });
}

@reflectiveTest
class InitializationTest extends AbstractLspAnalysisServerTest {
  /// The default value of [MessageScheduler.allowOverlappingHandlers] before
  /// and test sets it (so that [tearDown] can revert it).
  final allowOverlappingHandlersDefault =
      MessageScheduler.allowOverlappingHandlers;

  var isAnalysisOptionsSelector = TypeMatcher<TextDocumentFilterScheme>()
      .having(
        (selector) => selector.pattern,
        'pattern',
        '**/analysis_options.yaml',
      );

  var isDartSelector = TypeMatcher<TextDocumentFilterScheme>().having(
    (selector) => selector.language,
    'language',
    'dart',
  );

  var isPubspecSelector = TypeMatcher<TextDocumentFilterScheme>().having(
    (selector) => selector.pattern,
    'pattern',
    '**/pubspec.yaml',
  );

  /// Waits for any in-progress analysis context rebuild.
  ///
  /// Pumps the event queue before and after, to ensure any server code that
  /// runs after the rebuild has had chance to run.
  Future<void> get contextRebuildComplete async {
    await pumpEventQueue(times: 5000);
    await server.analysisContextsRebuilt;
    await pumpEventQueue(times: 5000);
  }

  Future<void> assertDynamicRegistration(
    String name,
    Set<Method> expectedResult,
  ) async {
    setTextDocumentDynamicRegistration(name);
    setWorkspaceDynamicRegistration(name);

    // Check that when the server calls client/registerCapability it only includes
    // the items we advertised dynamic registration support for.
    var registrations = <Registration>[];
    await monitorDynamicRegistrations(registrations, initialize);

    var registeredMethods = registrations
        .map((registration) => registration.method)
        .toSet();
    var result = expectedResult.map((method) => method.toJson()).toSet();

    expect(registeredMethods, equals(result));
  }

  /// Expects successful initialization, but a window/showMessage warning
  /// [expectedMessage] for the given [experimentalCapabilities].
  Future<void> expectInvalidExperimentalParams(
    Map<String, Object?> experimentalCapabilities,
    String expectedMessage,
  ) async {
    var warning = showMessageNotifications.firstWhere(
      (m) => m.type == MessageType.Warning,
    );

    await initialize(experimentalCapabilities: experimentalCapabilities);
    var message = (await warning).message;

    expect(
      message,
      allOf(
        contains('errors parsing your experimental client capabilities'),
        contains(expectedMessage),
      ),
    );
  }

  TextDocumentRegistrationOptions registrationOptionsFor(
    List<Registration> registrations,
    Method method,
  ) {
    var options = registrationFor(registrations, method)?.registerOptions;
    if (options == null) {
      throw 'Registration options for $method were not found. '
          'Perhaps dynamicRegistration is missing from '
          'setAllSupportedTextDocumentDynamicRegistrations?';
    }
    return TextDocumentRegistrationOptions.fromJson(
      options as Map<String, Object?>,
    );
  }

  @override
  Future<void> tearDown() async {
    await super.tearDown();
    MessageScheduler.allowOverlappingHandlers = allowOverlappingHandlersDefault;
  }

  Future<void> test_blazeWorkspace() async {
    var workspacePath = '/home/user/ws';
    // Make it a Blaze workspace.
    newFile('$workspacePath/${file_paths.blazeWorkspaceMarker}', '');

    var packagePath = '$workspacePath/team/project1';

    // Make it a Blaze project.
    newFile(convertPath('$packagePath/BUILD'), '');

    var file1 = convertPath('$packagePath/lib/file1.dart');
    newFile(file1, '');

    await initialize(allowEmptyRootUri: true);

    // Expect that context manager includes a whole package.
    await openFile(pathContext.toUri(file1), '');
    expect(
      server.contextManager.includedPaths,
      equals([convertPath(packagePath)]),
    );
  }

  Future<void> test_completionRegistrations_triggerCharacters() async {
    // Support dynamic registration for everything we support.
    setAllSupportedTextDocumentDynamicRegistrations();
    setAllSupportedWorkspaceDynamicRegistrations();

    var registrations = <Registration>[];
    var initResponse = await monitorDynamicRegistrations(
      registrations,
      () => initialize(),
    );

    var initResult = InitializeResult.fromJson(
      initResponse.result as Map<String, Object?>,
    );
    expect(initResult.capabilities, isNotNull);

    // Check Dart-only registration.
    var dartRegistration = registrationForDart(
      registrations,
      Method.textDocument_completion,
    );
    var dartOptions = CompletionRegistrationOptions.fromJson(
      dartRegistration.registerOptions as Map<String, Object?>,
    );
    expect(dartOptions.documentSelector, hasLength(1));
    expect(dartOptions.documentSelector![0].language, dartLanguageId);
    expect(dartOptions.triggerCharacters, isNotEmpty);

    // Check non-Dart registration.
    var nonDartRegistration = registrations.singleWhere(
      (r) =>
          r.method == Method.textDocument_completion.toJson() &&
          r != dartRegistration,
    );
    var nonDartOptions = CompletionRegistrationOptions.fromJson(
      nonDartRegistration.registerOptions as Map<String, Object?>,
    );
    var otherLanguages = nonDartOptions.documentSelector!
        .map((selector) => selector.language)
        .toList();
    expect(otherLanguages, isNot(contains('dart')));
    expect(nonDartOptions.triggerCharacters, isNull);
  }

  Future<void> test_completionRegistrations_withDartPlugin() async {
    // This tests for a bug that occurred with an analysis server plugin
    // that works on Dart files. When computing completion registrations we
    // usually have separate registrations for Dart + non-Dart to account for
    // different trigger characters. However, the plugin types were all being
    // included in the non-Dart registration even if they included Dart.
    //
    // The result was two registrations including Dart, which caused duplicate
    // requests for Dart completions, which resulted in duplicate items
    // appearing in the editor.

    setAllSupportedTextDocumentDynamicRegistrations();

    // Track all current registrations.
    var registrations = <Registration>[];

    // Perform normal registration (without plugins) to get the initial set.
    await monitorDynamicRegistrations(registrations, initialize);

    // Expect only a single registration that includes Dart files.
    expect(
      registrationsForDart(registrations, Method.textDocument_completion),
      hasLength(1),
    );

    // Monitor the unregistration/new registrations during the plugin activation.
    await monitorDynamicReregistration(registrations, () async {
      var plugin = configureTestPlugin();
      plugin.currentSession = PluginSession(plugin)
        ..interestingFileGlobs = ['*.dart'];
      pluginManager.pluginsChangedController.add(null);
    });

    // Expect that there is still only a single registration for Dart.
    expect(
      registrationsForDart(registrations, Method.textDocument_completion),
      hasLength(1),
    );
  }

  Future<void> test_dynamicRegistration_areNotInterleaved() async {
    // Some of the issues in https://github.com/dart-lang/sdk/issues/47851
    // (duplicate hovers/code actions/etc.) were caused by duplicate
    // registrations. This happened when we tried to rebuild registrations
    // concurrently as the code would:
    //
    // 1. compute the new set of registrations
    // 2. compute which had changed
    // 3. send _and await_ the unregistration
    // 4. send _and await_ the new registration
    //
    // If the code was triggered multiple times concurrently, it was possible
    // for step 3 in second request to occur before step 4 in the first, which
    // would result in unregistrations being sent for registrations that had
    // not been sent. They would be silently dropped, and then we'd end up with
    // multiple registrations.
    //
    // This test triggers the rebuild via pluginManager.pluginsChangedController
    // multiple times and ensures the events all arrive in the correct order,
    // that is, no unregistration ever contains the ID of something that is not
    // currently registered.

    setAllSupportedTextDocumentDynamicRegistrations();

    var registrations = <Registration>[];
    await monitorDynamicRegistrations(registrations, initialize);

    var knownRegistrationsIds = registrations
        .map((registration) => registration.id)
        .toSet();

    var numberOfReregistrations = 10;

    // Listen to incoming registrations, ensure they're not in the set, and add
    // them.
    var registrationsDone = requestsFromServer
        .where((n) => n.method == Method.client_registerCapability)
        .take(numberOfReregistrations)
        .listen((request) {
          respondTo(request, null);
          var registrations = RegistrationParams.fromJson(
            request.params as Map<String, Object?>,
          ).registrations;
          for (var registration in registrations) {
            var id = registration.id;
            if (!knownRegistrationsIds.add(id)) {
              throw 'Registration $id was already in the existing set!';
            }
          }
        })
        .asFuture();

    // Listen to incoming unregistrations, verify they're in the set, and remove
    // them.
    var unregistrationsDone = requestsFromServer
        .where((n) => n.method == Method.client_unregisterCapability)
        .take(numberOfReregistrations)
        .listen((request) {
          respondTo(request, null);
          var unregistrations = UnregistrationParams.fromJson(
            request.params as Map<String, Object?>,
          ).unregisterations;
          for (var unregistration in unregistrations) {
            var id = unregistration.id;
            if (!knownRegistrationsIds.remove(id)) {
              throw 'Registration $id was not in the existing set!';
            }
          }
        })
        .asFuture();

    // Trigger multiple plugin events that will rebuild the registrations.
    for (var i = 0; i < numberOfReregistrations; i++) {
      var plugin = configureTestPlugin();
      plugin.currentSession = PluginSession(plugin);
      // Ensure they have different file types so the registrations change,
      // otherwise they will be optimised out as not changing.
      plugin.currentSession!.interestingFileGlobs = ['*.foo$i'];
      pluginManager.pluginsChangedController.add(null);
      await null; // Allow the server to begin processing the change.
    }

    // Wait for both streams to have handled all of the numberOfReregistrations
    // expected events (without throwing).
    await Future.wait([registrationsDone, unregistrationsDone]);
  }

  Future<void> test_dynamicRegistration_config_allHierarchy() =>
      assertDynamicRegistration('callHierarchy', {
        Method.textDocument_prepareCallHierarchy,
      });

  Future<void> test_dynamicRegistration_config_codeAction() =>
      assertDynamicRegistration('codeAction', {Method.textDocument_codeAction});

  Future<void> test_dynamicRegistration_config_codeLens() =>
      assertDynamicRegistration('codeLens', {Method.textDocument_codeLens});

  Future<void> test_dynamicRegistration_config_colorProvider() =>
      assertDynamicRegistration('colorProvider', {
        Method.textDocument_documentColor,
      });

  Future<void> test_dynamicRegistration_config_completion() =>
      assertDynamicRegistration('completion', {Method.textDocument_completion});

  Future<void> test_dynamicRegistration_config_definition() =>
      assertDynamicRegistration('definition', {Method.textDocument_definition});

  Future<void> test_dynamicRegistration_config_didChangeConfiguration() =>
      assertDynamicRegistration('didChangeConfiguration', {
        Method.workspace_didChangeConfiguration,
      });

  Future<void> test_dynamicRegistration_config_documentHighlight() =>
      assertDynamicRegistration('documentHighlight', {
        Method.textDocument_documentHighlight,
      });

  Future<void> test_dynamicRegistration_config_documentLink() =>
      assertDynamicRegistration('documentLink', {
        Method.textDocument_documentLink,
      });

  Future<void> test_dynamicRegistration_config_documentSymbol() =>
      assertDynamicRegistration('documentSymbol', {
        Method.textDocument_documentSymbol,
      });

  Future<void> test_dynamicRegistration_config_fileOperations() =>
      assertDynamicRegistration('fileOperations', {
        Method.workspace_willRenameFiles,
      });

  Future<void> test_dynamicRegistration_config_foldingRange() =>
      assertDynamicRegistration('foldingRange', {
        Method.textDocument_foldingRange,
      });

  Future<void> test_dynamicRegistration_config_formatting() =>
      assertDynamicRegistration('formatting', {Method.textDocument_formatting});

  Future<void> test_dynamicRegistration_config_hover() =>
      assertDynamicRegistration('hover', {Method.textDocument_hover});

  Future<void> test_dynamicRegistration_config_implementation() =>
      assertDynamicRegistration('implementation', {
        Method.textDocument_implementation,
      });

  Future<void> test_dynamicRegistration_config_inlayHint() =>
      assertDynamicRegistration('inlayHint', {Method.textDocument_inlayHint});

  Future<void> test_dynamicRegistration_config_onTypeFormatting() =>
      assertDynamicRegistration('onTypeFormatting', {
        Method.textDocument_onTypeFormatting,
      });

  Future<void> test_dynamicRegistration_config_rangeFormatting() =>
      assertDynamicRegistration('rangeFormatting', {
        Method.textDocument_rangeFormatting,
      });

  Future<void> test_dynamicRegistration_config_references() =>
      assertDynamicRegistration('references', {Method.textDocument_references});

  Future<void> test_dynamicRegistration_config_rename() =>
      assertDynamicRegistration('rename', {Method.textDocument_rename});

  Future<void> test_dynamicRegistration_config_selectionRange() =>
      assertDynamicRegistration('selectionRange', {
        Method.textDocument_selectionRange,
      });

  Future<void> test_dynamicRegistration_config_semanticTokens() =>
      assertDynamicRegistration('semanticTokens', {
        CustomMethods.semanticTokenDynamicRegistration,
      });

  Future<void> test_dynamicRegistration_config_signatureHelp() =>
      assertDynamicRegistration('signatureHelp', {
        Method.textDocument_signatureHelp,
      });

  Future<void> test_dynamicRegistration_config_synchronization() =>
      assertDynamicRegistration('synchronization', {
        Method.textDocument_didOpen,
        Method.textDocument_didChange,
        Method.textDocument_didClose,
      });

  Future<void> test_dynamicRegistration_config_typeDefinition() =>
      assertDynamicRegistration('typeDefinition', {
        Method.textDocument_typeDefinition,
      });

  Future<void> test_dynamicRegistration_config_typeHierarchy() =>
      assertDynamicRegistration('typeHierarchy', {
        Method.textDocument_prepareTypeHierarchy,
      });

  Future<void> test_dynamicRegistration_containsAppropriateSettings() async {
    // Support file operations.
    setFileOperationDynamicRegistration();
    // Support dynamic registration for text sync, hovers, and code actions.
    setTextSyncDynamicRegistration();
    setHoverDynamicRegistration();
    setTextDocumentDynamicRegistration('codeAction');

    // Basic check that the server responds with the capabilities we'd expect,
    // for ex including analysis_options.yaml in text synchronization (and code
    // actions) but not for hovers.
    var registrations = <Registration>[];
    var initResponse = await monitorDynamicRegistrations(
      registrations,
      initialize,
    );

    // Because we support dynamic registration for synchronization, we won't send
    // static registrations for them.
    // https://github.com/dart-lang/sdk/issues/38490
    var initResult = InitializeResult.fromJson(
      initResponse.result as Map<String, Object?>,
    );
    expect(initResult.serverInfo!.name, 'Dart SDK LSP Analysis Server');
    expect(initResult.serverInfo!.version, isNotNull);
    expect(initResult.capabilities, isNotNull);
    expect(initResult.capabilities.textDocumentSync, isNull);

    // Should contain Hover, DidOpen, DidClose, DidChange, CodeAction,
    // WillRenameFiles.
    expect(registrations, hasLength(6));
    var hover = registrationOptionsFor(
      registrations,
      Method.textDocument_hover,
    );
    var change = registrationOptionsFor(
      registrations,
      Method.textDocument_didChange,
    );
    var codeAction = registrationOptionsFor(
      registrations,
      Method.textDocument_codeAction,
    );
    var rename = FileOperationRegistrationOptions.fromJson(
      registrationFor(
            registrations,
            Method.workspace_willRenameFiles,
          )?.registerOptions
          as Map<String, Object?>,
    );
    expect(
      registrationOptionsFor(registrations, Method.textDocument_didOpen),
      isNotNull,
    );
    expect(
      registrationOptionsFor(registrations, Method.textDocument_didClose),
      isNotNull,
    );

    // The hover capability should only specific Dart.
    expect(hover, isNotNull);
    expect(hover.documentSelector, hasLength(1));
    expect(hover.documentSelector!.single, isDartSelector);

    // didChange should also include pubspec + analysis_options.
    expect(change, isNotNull);
    expect(change.documentSelector, hasLength(greaterThanOrEqualTo(3)));
    expect(
      change.documentSelector,
      containsAll([
        isDartSelector,
        isPubspecSelector,
        isAnalysisOptionsSelector,
      ]),
    );

    // codeAction should also include pubspec + analysis_options.
    expect(codeAction, isNotNull);
    expect(codeAction.documentSelector, hasLength(greaterThanOrEqualTo(3)));
    expect(
      change.documentSelector,
      containsAll([
        isDartSelector,
        isPubspecSelector,
        isAnalysisOptionsSelector,
      ]),
    );

    expect(rename, equals(fileOperationRegistrationOptions));
  }

  Future<void> test_dynamicRegistration_notSupportedByClient() async {
    // If the client doesn't send any dynamicRegistration settings then there
    // should be no `client/registerCapability` calls.

    // Set a flag if any registerCapability request comes through.
    var didGetRegisterCapabilityRequest = false;
    requestsFromServer
        .where((n) => n.method == Method.client_registerCapability)
        .listen((_) => didGetRegisterCapabilityRequest = true);

    // Initialize with no dynamic registrations advertised.
    var initResponse = await initialize();
    await pumpEventQueue();

    var initResult = InitializeResult.fromJson(
      initResponse.result as Map<String, Object?>,
    );
    expect(initResult.capabilities, isNotNull);
    // When dynamic registration is not supported, we will always statically
    // request text document open/close and incremental updates.
    expect(initResult.capabilities.textDocumentSync, isNotNull);
    initResult.capabilities.textDocumentSync!.map(
      (_) =>
          throw 'Expected textDocumentSync capabilities to be a TextDocumentSyncOptions',
      (options) {
        expect(options.openClose, isTrue);
        expect(options.change, equals(TextDocumentSyncKind.Incremental));
      },
    );
    expect(initResult.capabilities.callHierarchyProvider, isNotNull);
    expect(initResult.capabilities.completionProvider, isNotNull);
    expect(initResult.capabilities.hoverProvider, isNotNull);
    expect(initResult.capabilities.signatureHelpProvider, isNotNull);
    expect(initResult.capabilities.referencesProvider, isNotNull);
    expect(initResult.capabilities.colorProvider, isNotNull);
    expect(initResult.capabilities.documentHighlightProvider, isNotNull);
    expect(initResult.capabilities.documentFormattingProvider, isNotNull);
    expect(initResult.capabilities.documentOnTypeFormattingProvider, isNotNull);
    expect(initResult.capabilities.documentRangeFormattingProvider, isNotNull);
    expect(initResult.capabilities.definitionProvider, isNotNull);
    expect(initResult.capabilities.codeActionProvider, isNotNull);
    expect(initResult.capabilities.renameProvider, isNotNull);
    expect(initResult.capabilities.foldingRangeProvider, isNotNull);
    expect(
      initResult.capabilities.workspace!.fileOperations!.willRename,
      equals(fileOperationRegistrationOptions),
    );
    expect(initResult.capabilities.selectionRangeProvider, isNotNull);
    expect(initResult.capabilities.semanticTokensProvider, isNotNull);

    expect(didGetRegisterCapabilityRequest, isFalse);
  }

  Future<void> test_dynamicRegistration_suppressesStaticRegistration() async {
    // Support dynamic registration for everything we support.
    setAllSupportedTextDocumentDynamicRegistrations();
    setAllSupportedWorkspaceDynamicRegistrations();

    // If the client sends dynamicRegistration settings then there
    // should not be static registrations for the same capabilities.
    var registrations = <Registration>[];
    var initResponse = await monitorDynamicRegistrations(
      registrations,
      initialize,
    );

    var initResult = InitializeResult.fromJson(
      initResponse.result as Map<String, Object?>,
    );
    expect(initResult.capabilities, isNotNull);

    // Ensure no static registrations. This list should include all server equivalents
    // of the dynamic registrations listed in `ClientDynamicRegistrations.supported`.
    expect(initResult.capabilities.textDocumentSync, isNull);
    expect(initResult.capabilities.callHierarchyProvider, isNull);
    expect(initResult.capabilities.completionProvider, isNull);
    expect(initResult.capabilities.hoverProvider, isNull);
    expect(initResult.capabilities.inlayHintProvider, isNull);
    expect(initResult.capabilities.signatureHelpProvider, isNull);
    expect(initResult.capabilities.referencesProvider, isNull);
    expect(initResult.capabilities.colorProvider, isNull);
    expect(initResult.capabilities.documentHighlightProvider, isNull);
    expect(initResult.capabilities.documentFormattingProvider, isNull);
    expect(initResult.capabilities.documentOnTypeFormattingProvider, isNull);
    expect(initResult.capabilities.documentRangeFormattingProvider, isNull);
    expect(initResult.capabilities.definitionProvider, isNull);
    expect(initResult.capabilities.codeActionProvider, isNull);
    expect(initResult.capabilities.renameProvider, isNull);
    expect(initResult.capabilities.foldingRangeProvider, isNull);
    expect(initResult.capabilities.workspace!.fileOperations, isNull);
    expect(initResult.capabilities.selectionRangeProvider, isNull);
    expect(initResult.capabilities.semanticTokensProvider, isNull);

    // Ensure all expected dynamic registrations.
    for (var expectedRegistration in ClientDynamicRegistrations.supported) {
      // We have two completion registrations (to handle different trigger
      // characters), so exclude that here and check it manually below.
      if (expectedRegistration == Method.textDocument_completion) {
        continue;
      }

      var registration = registrationOptionsFor(
        registrations,
        expectedRegistration,
      );
      expect(
        registration,
        isNotNull,
        reason: 'Missing dynamic registration for $expectedRegistration',
      );
    }

    // Check the were two completion registrations.
    var completionRegistrations = registrations.where(
      (reg) => reg.method == Method.textDocument_completion.toJson(),
    );
    expect(completionRegistrations, hasLength(2));
  }

  Future<void> test_dynamicRegistration_unregistersOutdatedAfterChange() async {
    // Initialize by supporting dynamic registrations everywhere
    setAllSupportedTextDocumentDynamicRegistrations();

    var registrations = <Registration>[];
    await monitorDynamicRegistrations(registrations, initialize);

    var unregisterRequest = await expectRequest(
      Method.client_unregisterCapability,
      () {
        var plugin = configureTestPlugin();
        plugin.currentSession = PluginSession(plugin)
          ..interestingFileGlobs = ['*.foo'];
        pluginManager.pluginsChangedController.add(null);
      },
    );
    var unregistrations = UnregistrationParams.fromJson(
      unregisterRequest.params as Map<String, Object?>,
    ).unregisterations;

    // folding method should have been unregistered as the server now supports
    // *.foo files for it as well.
    var registrationIdForFolding = registrations
        .singleWhere((r) => r.method == 'textDocument/foldingRange')
        .id;
    expect(
      unregistrations,
      contains(
        isA<Unregistration>()
            .having((r) => r.method, 'method', 'textDocument/foldingRange')
            .having((r) => r.id, 'id', registrationIdForFolding),
      ),
    );
  }

  Future<void> test_dynamicRegistration_updatesWithPlugins() async {
    setTextDocumentDynamicRegistration('foldingRange');
    await initialize();

    // The server will send an unregister request followed by another register
    // request to change document filter on folding. We need to respond to the
    // unregister request as the server awaits that.
    // This is set up as a future callback and should not be awaited here.
    unawaited(
      requestsFromServer
          .firstWhere((r) => r.method == Method.client_unregisterCapability)
          .then((request) {
            respondTo(request, null);
          }),
    );

    var request = await expectRequest(Method.client_registerCapability, () {
      var plugin = configureTestPlugin();
      plugin.currentSession = PluginSession(plugin)
        ..interestingFileGlobs = ['*.sql'];
      pluginManager.pluginsChangedController.add(null);
    });

    var registrations = RegistrationParams.fromJson(
      request.params as Map<String, Object?>,
    ).registrations;

    var documentFilterSql = TextDocumentFilterScheme(
      scheme: 'file',
      pattern: '**/*.sql',
    );
    var documentFilterDart = TextDocumentFilterScheme(
      language: 'dart',
      scheme: 'file',
    );

    expect(
      registrations,
      contains(
        isA<Registration>()
            .having((r) => r.method, 'method', 'textDocument/foldingRange')
            .having(
              (r) => TextDocumentRegistrationOptions.fromJson(
                r.registerOptions as Map<String, Object?>,
              ).documentSelector,
              'registerOptions.documentSelector',
              containsAll([documentFilterSql, documentFilterDart]),
            ),
      ),
    );
  }

  /// Tests that when there are no explicit analysis roots (instead they are
  /// implied by open files), requests for open files are successful even if
  /// sent _immediately_ after opening the file.
  ///
  /// https://github.com/Dart-Code/Dart-Code/issues/3929
  ///
  /// This test uses getDocumentSymbols which requires a resolved result.
  Future<void>
  test_emptyAnalysisRoots_handlesFileRequestsImmediately_resolved() async {
    const content = 'void f() {}';
    var file1 = join(projectFolderPath, 'file1.dart');
    var file1Uri = pathContext.toUri(file1);
    newFile(file1, content);
    newPubspecYamlFile(projectFolderPath, '');

    await initialize(allowEmptyRootUri: true);

    unawaited(openFile(file1Uri, content)); // Don't wait
    var result = await getDocumentSymbols(file1Uri);
    var symbols = result.map(
      (docSymbols) => docSymbols,
      (symbolInfos) => symbolInfos,
    );
    expect(symbols, hasLength(1));
  }

  /// Tests that when there are no explicit analysis roots (instead they are
  /// implied by open files), requests for open files are successful even if
  /// sent _immediately_ after opening the file.
  ///
  /// https://github.com/Dart-Code/Dart-Code/issues/3929
  ///
  /// This test uses getSelectionRanges which requires only a parsed result.
  Future<void>
  test_emptyAnalysisRoots_handlesFileRequestsImmediately_unresolved() async {
    const content = 'void f() {}';
    var file1 = join(projectFolderPath, 'file1.dart');
    var file1Uri = pathContext.toUri(file1);
    newFile(file1, content);
    newPubspecYamlFile(projectFolderPath, '');

    await initialize(allowEmptyRootUri: true);

    unawaited(openFile(file1Uri, content)); // Don't wait
    var result = await getSelectionRanges(file1Uri, startOfDocPos);
    expect(result, isNotNull);
  }

  Future<void> test_emptyAnalysisRoots_multipleOpenFiles() async {
    var file1 = join(projectFolderPath, 'file1.dart');
    var file1Uri = pathContext.toUri(file1);
    newFile(file1, '');
    var file2 = join(projectFolderPath, 'file2.dart');
    var file2Uri = pathContext.toUri(file2);
    newFile(file2, '');
    newPubspecYamlFile(projectFolderPath, '');

    await initialize(allowEmptyRootUri: true);

    // Opening both files should only add the project folder once.
    await openFile(file1Uri, '');
    await openFile(file2Uri, '');
    await contextRebuildComplete;
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));

    // Closing only one of the files should not remove the project folder
    // since there are still open files.
    resetContextBuildCounter();
    await closeFile(file1Uri);
    await pumpEventQueue(times: 5000);
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
    expectNoContextBuilds();

    // Closing the last file should remove the project folder and remove
    // the context.
    resetContextBuildCounter();
    await closeFile(file2Uri);
    await contextRebuildComplete;
    expect(server.contextManager.includedPaths, equals([]));
    expect(server.contextManager.driverMap, hasLength(0));
    expectContextBuilds();
  }

  Future<void> test_emptyAnalysisRoots_projectWithoutPubspec() async {
    projectFolderPath = convertPath('/home/empty');
    var nestedFilePath = join(
      projectFolderPath,
      'nested',
      'deeply',
      'in',
      'folders',
      'test.dart',
    );
    var nestedFileUri = pathContext.toUri(nestedFilePath);
    newFile(nestedFilePath, '');

    // The project folder shouldn't be added to start with.
    await initialize(allowEmptyRootUri: true);
    expect(server.contextManager.includedPaths, equals([]));

    // Opening the file will add a root for it.
    await openFile(nestedFileUri, '');
    expect(server.contextManager.includedPaths, equals([nestedFilePath]));
  }

  Future<void> test_emptyAnalysisRoots_projectWithPubspec() async {
    var nestedFilePath = join(
      projectFolderPath,
      'nested',
      'deeply',
      'in',
      'folders',
      'test.dart',
    );
    var nestedFileUri = pathContext.toUri(nestedFilePath);
    newFile(nestedFilePath, '');
    newPubspecYamlFile(projectFolderPath, '');

    // The project folder shouldn't be added to start with.
    await initialize(allowEmptyRootUri: true);
    expect(server.contextManager.includedPaths, equals([]));

    // Opening a file nested within the project should add the project folder.
    await openFile(nestedFileUri, '');
    await contextRebuildComplete;
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));

    // Ensure the file was cached in each driver. This happens as a result of
    // adding to priority files, but if that's done before the file is in an
    // analysis root it will not occur.
    // https://github.com/dart-lang/sdk/issues/37338
    for (var driver in server.driverMap.values) {
      expect(driver.getCachedResolvedUnit(nestedFilePath), isNotNull);
    }

    // Closing the file should remove it.
    await closeFile(nestedFileUri);
    expect(server.contextManager.includedPaths, equals([]));
  }

  Future<void> test_excludedFolders_absolute() async {
    var excludedFolderPath = join(projectFolderPath, 'excluded');

    await provideConfig(
      initialize,
      // Exclude the folder with a relative path.
      {
        'analysisExcludedFolders': [excludedFolderPath],
      },
    );
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
    expect(server.contextManager.excludedPaths, equals([excludedFolderPath]));
  }

  Future<void> test_excludedFolders_nonList() async {
    var excludedFolderPath = join(projectFolderPath, 'excluded');

    await provideConfig(
      initialize,
      // Include a single string instead of an array since it's an easy mistake
      // to make without editor validation of settings.
      {'analysisExcludedFolders': 'excluded'},
    );
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
    expect(server.contextManager.excludedPaths, equals([excludedFolderPath]));
  }

  Future<void> test_excludedFolders_relative() async {
    var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb');

    await provideConfig(
      initialize,
      // Exclude the folder with a relative path.
      {
        'analysisExcludedFolders': ['aaa', 'bbb'].join(pathContext.separator),
      },
    );
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
    expect(server.contextManager.excludedPaths, equals([excludedFolderPath]));
  }

  /// Check exclusions using explicit posix paths even if we're running
  /// on Windows. It's common for shared configs in VS Code to always use
  /// forward slashes in paths.
  Future<void> test_excludedFolders_relative_posix() async {
    var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb');

    await provideConfig(
      initialize,
      // Exclude the folder with a relative path using forward slashes.
      {
        'analysisExcludedFolders': ['aaa', 'bbb'].join('/'),
      },
    );
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
    expect(server.contextManager.excludedPaths, equals([excludedFolderPath]));
  }

  Future<void> test_excludedFolders_trailingSlash() async {
    var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb');

    await provideConfig(
      initialize,
      // Exclude the folder with a trailing slash.
      {
        'analysisExcludedFolders': [
          'aaa',
          'bbb',
          '',
        ].join(pathContext.separator),
      },
    );
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
    expect(server.contextManager.excludedPaths, equals([excludedFolderPath]));
  }

  /// Check exclusions using explicit posix paths even if we're running
  /// on Windows. It's common for shared configs in VS Code to always use
  /// forward slashes in paths.
  Future<void> test_excludedFolders_trailingSlash_posix() async {
    var excludedFolderPath = join(projectFolderPath, 'aaa', 'bbb');

    await provideConfig(
      initialize,
      // Exclude the folder with a forward trailing slash.
      {
        'analysisExcludedFolders': ['aaa', 'bbb', ''].join('/'),
      },
    );
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
    expect(server.contextManager.excludedPaths, equals([excludedFolderPath]));
  }

  /// Tests that requests that requires a unit result are handled correctly even
  /// if sent immediately after the `initialized` notification and do not result
  /// in "File not Analyzed"-style errors because roots are set asynchronously.
  Future<void> test_immediateRequests() async {
    newFile(mainFilePath, 'void f() {}');
    late Future<Either2<List<DocumentSymbol>, List<SymbolInformation>>> result;
    await provideConfig(
      () => initialize(
        immediatelyAfterInitialized: () {
          result = getDocumentSymbols(mainFileUri);
        },
      ),
      {},
    );

    var symbols = (await result).map(
      (docSymbols) => docSymbols,
      (symbolInfos) => symbolInfos,
    );
    expect(symbols, hasLength(1));
  }

  Future<void> test_initialize() async {
    var response = await initialize();
    expect(response, isNotNull);
    expect(response.error, isNull);
    expect(response.result, isNotNull);
    expect(
      InitializeResult.canParse(response.result, nullLspJsonReporter),
      isTrue,
    );
    var result = InitializeResult.fromJson(
      response.result as Map<String, Object?>,
    );
    expect(result.capabilities, isNotNull);
    // Check some basic capabilities that are unlikely to change.
    expect(result.capabilities.textDocumentSync, isNotNull);
    result.capabilities.textDocumentSync!.map(
      (_) =>
          throw 'Expected textDocumentSync capabilities to be a TextDocumentSyncOptions',
      (options) {
        // We'll always request open/closed notifications and incremental updates.
        expect(options.openClose, isTrue);
        expect(options.change, equals(TextDocumentSyncKind.Incremental));
      },
    );
  }

  Future<void> test_initialize_invalidParams() async {
    var params = {'processId': 'invalid'};
    var request = RequestMessage(
      id: Either2<int, String>.t1(1),
      method: Method.initialize,
      params: params,
      jsonrpc: jsonRpcVersion,
    );
    var response = await sendRequestToServer(request);
    expect(response.id, equals(request.id));
    expect(response.error, isNotNull);
    expect(response.error!.code, equals(ErrorCodes.InvalidParams));
    expect(response.result, isNull);
  }

  Future<void> test_initialize_onlyAllowedOnce() async {
    await initialize();
    var response = await initialize(throwOnFailure: false);
    expect(response, isNotNull);
    expect(response.result, isNull);
    expect(response.error, isNotNull);
    expect(
      response.error!.code,
      equals(ServerErrorCodes.serverAlreadyInitialized),
    );
  }

  Future<void> test_initialize_rootPath() async {
    await initialize(rootPath: projectFolderPath);
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
  }

  Future<void> test_initialize_rootPath_trailingSlash() async {
    await initialize(rootPath: withTrailingSlash(projectFolderPath));
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
  }

  Future<void> test_initialize_rootUri() async {
    await initialize(rootUri: projectFolderUri);
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
  }

  Future<void> test_initialize_rootUri_encodedDriveLetterColon() async {
    await initialize(rootUri: withEncodedDriveLetterColon(projectFolderUri));
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
  }

  Future<void> test_initialize_rootUri_trailingSlash() async {
    await initialize(rootUri: withTrailingSlashUri(projectFolderUri));
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
  }

  Future<void> test_initialize_workspaceFolders() async {
    await initialize(workspaceFolders: [projectFolderUri]);
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
  }

  Future<void>
  test_initialize_workspaceFolders_encodedDriveLetterColon() async {
    await initialize(
      workspaceFolders: [withEncodedDriveLetterColon(projectFolderUri)],
    );
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
  }

  Future<void> test_initialize_workspaceFolders_trailingSlash() async {
    await initialize(
      workspaceFolders: [withTrailingSlashUri(projectFolderUri)],
    );
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
  }

  Future<void> test_interleavedRequests_default() async {
    await initialize(rootUri: projectFolderUri, initializationOptions: {});
    expect(
      MessageScheduler.allowOverlappingHandlers,
      allowOverlappingHandlersDefault,
    );
  }

  Future<void> test_interleavedRequests_explicitFalse() async {
    await initialize(
      rootUri: projectFolderUri,
      initializationOptions: {'allowOverlappingHandlers': false},
    );
    expect(MessageScheduler.allowOverlappingHandlers, isFalse);
  }

  Future<void> test_interleavedRequests_explicitTrue() async {
    await initialize(
      rootUri: projectFolderUri,
      initializationOptions: {'allowOverlappingHandlers': true},
    );
    expect(MessageScheduler.allowOverlappingHandlers, isTrue);
  }

  Future<void> test_invalidExperimental_commands() async {
    await expectInvalidExperimentalParams({
      'commands': 1,
    }, 'ClientCapabilities.experimental.commands must be a List<String>?');
  }

  Future<void> test_invalidExperimental_snippetTextEdit() async {
    await expectInvalidExperimentalParams({
      'snippetTextEdit': 1,
    }, 'ClientCapabilities.experimental.snippetTextEdit must be a bool?');
  }

  Future<void> test_nonFileScheme_rootUri() async {
    // We expect an error notification about the invalid file we try to open.
    failTestOnAnyErrorNotification = false;

    var rootUri = Uri.parse('vsls://');
    var fileUri = rootUri.replace(path: '/file1.dart');

    await initialize(rootUri: rootUri);
    expect(server.contextManager.includedPaths, equals([]));

    // Also open a non-file file to ensure it doesn't cause the root to be added.
    await openFile(fileUri, '');
    expect(server.contextManager.includedPaths, equals([]));
  }

  /// Verifies a workspace that contains non-file workspace folders is handled.
  ///
  /// Related tests for didChangeWorkspaceFolders are in
  /// [ChangeWorkspaceFoldersTest].
  Future<void> test_nonFileScheme_workspaceFolders() async {
    // We expect an error notification about the invalid file we try to open.
    failTestOnAnyErrorNotification = false;
    newPubspecYamlFile(projectFolderPath, '');

    var rootUri = Uri.parse('vsls://');
    var fileUri = rootUri.replace(path: '/file1.dart');

    await initialize(
      workspaceFolders: [rootUri, pathContext.toUri(projectFolderPath)],
    );
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));

    // Also open a non-file file to ensure it doesn't cause the root to be added.
    await openFile(fileUri, '');
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
  }

  Future<void> test_nonProjectFiles_basicWorkspace() async {
    var file1 = convertPath('/home/nonProject/file1.dart');
    newFile(file1, '');

    await initialize(allowEmptyRootUri: true);

    // Because the file is not in a project, it should be added itself.
    await openFile(pathContext.toUri(file1), '');
    expect(server.contextManager.includedPaths, equals([file1]));
  }

  Future<void> test_nonProjectFiles_blazeWorkspace() async {
    var file1 = convertPath('/home/nonProject/file1.dart');
    newFile(file1, '');

    // Make /home a Blaze workspace.
    newFile('/home/${file_paths.blazeWorkspaceMarker}', '');

    await initialize(allowEmptyRootUri: true);

    // Because the file is not in a project, it should be added itself.
    await openFile(pathContext.toUri(file1), '');
    expect(server.contextManager.includedPaths, equals([file1]));
  }

  Future<void> test_onlyAnalyzeProjectsWithOpenFiles_fullyInitializes() async {
    // Enable some dynamic registrations, else registerCapability will not
    // be called.
    setAllSupportedTextDocumentDynamicRegistrations();

    // Ensure when we use onlyAnalyzeProjectsWithOpenFiles that we still
    // fully initialize (eg. capabilities are registered).
    projectFolderPath = convertPath('/home/empty');

    await expectRequest(
      Method.client_registerCapability,
      () => initialize(
        rootUri: projectFolderUri,
        initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true},
      ),
    );
  }

  Future<void> test_onlyAnalyzeProjectsWithOpenFiles_multipleFiles() async {
    var file1 = join(projectFolderPath, 'file1.dart');
    var file1Uri = pathContext.toUri(file1);
    newFile(file1, '');
    var file2 = join(projectFolderPath, 'file2.dart');
    var file2Uri = pathContext.toUri(file2);
    newFile(file2, '');
    newPubspecYamlFile(projectFolderPath, '');

    await initialize(
      rootUri: projectFolderUri,
      initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true},
    );

    // Opening both files should only add the project folder once.
    await openFile(file1Uri, '');
    await openFile(file2Uri, '');
    await contextRebuildComplete;
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
    expect(server.contextManager.driverMap, hasLength(1));

    // Closing only one of the files should not remove the root or rebuild the context.
    resetContextBuildCounter();
    await closeFile(file1Uri);
    await pumpEventQueue(times: 5000);
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));
    expect(server.contextManager.driverMap, hasLength(1));
    expectNoContextBuilds();

    // Closing the last file should remove the project folder and remove
    // the context.
    resetContextBuildCounter();
    await closeFile(file2Uri);
    await contextRebuildComplete;
    expect(server.contextManager.includedPaths, equals([]));
    expect(server.contextManager.driverMap, hasLength(0));
    expectContextBuilds();
  }

  Future<void> test_onlyAnalyzeProjectsWithOpenFiles_withoutPubpsec() async {
    projectFolderPath = convertPath('/home/empty');
    var nestedFilePath = join(
      projectFolderPath,
      'nested',
      'deeply',
      'in',
      'folders',
      'test.dart',
    );
    var nestedFileUri = pathContext.toUri(nestedFilePath);
    newFile(nestedFilePath, '');

    // The project folder shouldn't be added to start with.
    await initialize(
      rootUri: projectFolderUri,
      initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true},
    );
    expect(server.contextManager.includedPaths, equals([]));

    // Opening the file should trigger it to be added.
    await openFile(nestedFileUri, '');
    expect(server.contextManager.includedPaths, equals([nestedFilePath]));
  }

  Future<void> test_onlyAnalyzeProjectsWithOpenFiles_withPubpsec() async {
    var nestedFilePath = join(
      projectFolderPath,
      'nested',
      'deeply',
      'in',
      'folders',
      'test.dart',
    );
    var nestedFileUri = pathContext.toUri(nestedFilePath);
    newFile(nestedFilePath, '');
    newPubspecYamlFile(projectFolderPath, '');

    // The project folder shouldn't be added to start with.
    await initialize(
      rootUri: projectFolderUri,
      initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true},
    );
    expect(server.contextManager.includedPaths, equals([]));

    // Opening a file nested within the project should cause the project folder
    // to be added
    await openFile(nestedFileUri, '');
    await contextRebuildComplete;
    expect(server.contextManager.includedPaths, equals([projectFolderPath]));

    // Ensure the file was cached in each driver. This happens as a result of
    // adding to priority files, but if that's done before the file is in an
    // analysis root it will not occur.
    // https://github.com/dart-lang/sdk/issues/37338
    for (var driver in server.driverMap.values) {
      expect(driver.getCachedResolvedUnit(nestedFilePath), isNotNull);
    }

    // Closing the file should remove it.
    await closeFile(nestedFileUri);
    expect(server.contextManager.includedPaths, equals([]));
  }

  Future<void> test_survey_alwaysEnabledForLsp() async {
    await initialize();
    expect(server.surveyManager, isNotNull);
  }

  Future<void> test_uninitialized_dropsNotifications() async {
    var notification = makeNotification(
      Method.fromJson('randomNotification'),
      null,
    );
    var nextNotification = errorNotificationsFromServer.first;
    channel.sendNotificationToServer(notification);

    // Wait up to 1sec to ensure no error/log notifications were sent back.
    var didTimeout = false;
    var notificationFromServer = await nextNotification
        .then<NotificationMessage?>((notification) => notification)
        .timeout(
          const Duration(seconds: 1),
          onTimeout: () {
            didTimeout = true;
            return null;
          },
        );

    expect(notificationFromServer, isNull);
    expect(didTimeout, isTrue);
  }

  Future<void> test_uninitialized_rejectsRequests() async {
    var request = makeRequest(Method.fromJson('randomRequest'), null);
    var response = await channel.sendRequestToServer(request);
    expect(response.id, equals(request.id));
    expect(response.result, isNull);
    expect(response.error, isNotNull);
    expect(response.error!.code, ErrorCodes.ServerNotInitialized);
  }
}

/// Runs all initialization tests with a resource provider that slowly
/// initializes watchers to simulate delays in real fs watchers.
@reflectiveTest
class SlowInitializationTest extends InitializationTest {
  @override
  MemoryResourceProvider resourceProvider = MemoryResourceProvider(
    // Force the in-memory file watchers to be slowly initialized to emulate
    // the physical watchers (for test_concurrentContextRebuilds).
    delayWatcherInitialization: Duration(milliseconds: 1),
  );
}

/// Runs all initialization tests using String IDs instead of integers (both
/// are valid).
@reflectiveTest
class StringIdInitializationTest extends InitializationTest {
  @override
  LspMessageIdMode get idMode => LspMessageIdMode.string;
}
