// 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/services/correction/assist_internal.dart';
import 'package:analysis_server/src/services/correction/fix_internal.dart';
import 'package:analyzer/src/lint/registry.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
import 'package:analyzer_testing/package_config_file_builder.dart';
import 'package:linter/src/rules.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../utils/test_code_extensions.dart';
import 'server_abstract.dart';

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

typedef _VoidCallback = Future<void> Function();

@reflectiveTest
class DiagnosticTest extends AbstractLspAnalysisServerTest {
  Future<void> checkPluginErrorsForFile(String pluginAnalyzedFilePath) async {
    var pluginAnalyzedUri = pathContext.toUri(pluginAnalyzedFilePath);

    var code = TestCode.parseNormalized('''/*[0*/String/*0]*/ a = "Test";
String b = "/*[1*/Test/*1]*/";
''');
    newFile(pluginAnalyzedFilePath, code.code);
    await initialize();

    var errorRange = code.ranges[0];
    var relatedRange = code.ranges[1];

    var diagnosticsUpdate = waitForDiagnostics(pluginAnalyzedUri);
    var pluginError = plugin.AnalysisError(
      plugin.AnalysisErrorSeverity.ERROR,
      plugin.AnalysisErrorType.STATIC_TYPE_WARNING,
      plugin.Location(
        pluginAnalyzedFilePath,
        errorRange.sourceRange.offset,
        errorRange.sourceRange.length,
        // LSP types are zero-based but server is one-based.
        errorRange.range.start.line + 1,
        errorRange.range.start.character + 1,
        endLine: errorRange.range.end.line + 1,
        endColumn: errorRange.range.end.character + 1,
      ),
      'Test error from plugin',
      'ERR1',
      contextMessages: [
        plugin.DiagnosticMessage(
          'Related error',
          plugin.Location(
            pluginAnalyzedFilePath,
            relatedRange.sourceRange.offset,
            relatedRange.sourceRange.length,
            // LSP types are zero-based but server is one-based.
            relatedRange.range.start.line + 1,
            relatedRange.range.start.character + 1,
            endLine: relatedRange.range.end.line + 1,
            endColumn: relatedRange.range.end.character + 1,
          ),
        ),
      ],
    );
    var pluginResult = plugin.AnalysisErrorsParams(pluginAnalyzedFilePath, [
      pluginError,
    ]);
    configureTestPlugin(notification: pluginResult.toNotification());

    var diagnostics = await diagnosticsUpdate;
    expect(diagnostics, hasLength(1));

    var err = diagnostics!.first;
    expect(err.severity, DiagnosticSeverity.Error);
    expect(err.message, equals('Test error from plugin'));
    expect(err.code, equals('ERR1'));
    expect(err.range.start.line, errorRange.range.start.line);
    expect(err.range.start.character, errorRange.range.start.character);
    expect(err.range.end.line, errorRange.range.end.line);
    expect(err.range.end.character, errorRange.range.end.character);
    expect(err.relatedInformation, hasLength(1));

    var related = err.relatedInformation![0];
    expect(related.message, equals('Related error'));
    // Server types are one-based but LSP is zero-based.
    expect(related.location.range.start.line, relatedRange.range.start.line);
    expect(
      related.location.range.start.character,
      relatedRange.range.start.character,
    );
    expect(related.location.range.end.line, relatedRange.range.end.line);
    expect(
      related.location.range.end.character,
      relatedRange.range.end.character,
    );
  }

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

    if (Registry.ruleRegistry.isEmpty) {
      registerLintRules();
    }

    // These tests deliberately generate diagnostics.
    failTestOnErrorDiagnostic = false;
  }

  Future<void> test_afterDocumentEdits() async {
    const initialContents = 'int a = 1;';
    newFile(mainFilePath, initialContents);

    await initialize();
    await openFile(mainFileUri, initialContents);
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNull);

    await replaceFile(222, mainFileUri, 'String a = 1;');
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNotEmpty);
  }

  Future<void> test_analysisOptionsFile() async {
    newFile(analysisOptionsPath, '''
linter:
  rules:
    - invalid_lint_rule_name
''');

    var firstDiagnosticsUpdate = waitForDiagnostics(analysisOptionsUri);
    await initialize();
    var initialDiagnostics = await firstDiagnosticsUpdate;
    expect(initialDiagnostics, hasLength(1));
    expect(initialDiagnostics!.first.severity, DiagnosticSeverity.Warning);
    expect(initialDiagnostics.first.code, 'undefined_lint');
  }

  @FailingTest(issue: 'https://github.com/dart-lang/sdk/issues/43926')
  Future<void> test_analysisOptionsFile_packageInclude() async {
    newFile(analysisOptionsPath, '''
include: package:pedantic/analysis_options.yaml
''');

    // Verify there's an error for the import.
    var firstDiagnosticsUpdate = waitForDiagnostics(analysisOptionsUri);
    await initialize();
    var initialDiagnostics = await firstDiagnosticsUpdate;
    expect(initialDiagnostics, hasLength(1));
    expect(initialDiagnostics!.first.severity, DiagnosticSeverity.Warning);
    expect(initialDiagnostics.first.code, 'include_file_not_found');

    // TODO(scheglov): The server does not handle the file change.
    throw 'Times out';

    // // Write a package file that allows resolving the include.
    // final secondDiagnosticsUpdate = waitForDiagnostics(analysisOptionsUri);
    // writeTestPackageConfig(pedantic: true);
    //
    // // Ensure the error disappeared.
    // final updatedDiagnostics = await secondDiagnosticsUpdate;
    // expect(updatedDiagnostics, hasLength(0));
  }

  /// Ensure the server can initialize correctly and send diagnostics when the
  /// analysis_options file throws errors during parsing.
  ///
  /// https://github.com/dart-lang/sdk/issues/55987
  Future<void> test_analysisOptionsFile_parseError() async {
    newFile(analysisOptionsPath, '''
include: package:lints/recommended.yaml
f

''');

    var firstDiagnosticsUpdate = waitForDiagnostics(analysisOptionsUri);
    await initialize();
    var initialDiagnostics = await firstDiagnosticsUpdate;
    var diagnostic = initialDiagnostics!.first;
    expect(diagnostic.severity, DiagnosticSeverity.Error);
    expect(diagnostic.code, 'parse_error');
    expect(diagnostic.message, "Expected ':'.");
  }

  Future<void> test_contextMessage() async {
    newFile(mainFilePath, '''
void f() {
  x = 0;
  int? x;
  print(x);
}
''');

    var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await initialize();
    var diagnostics = await diagnosticsUpdate;
    expect(diagnostics, hasLength(1));
    var diagnostic = diagnostics!.first;
    expect(diagnostic.relatedInformation, hasLength(1));
  }

  Future<void> test_contextMessage_unnamedElement_issue62955() async {
    var code = TestCode.parseNormalized('''
void main() {
  [].named;
}

/*[0*//*0]*/extension<T> on List<T> {
  Iterable<T> get named => throw 0;
}

/*[1*//*1]*/extension<T> on List<T> {
  T? named(String? name) => throw 0;
}
''');
    newFile(mainFilePath, code.code);

    var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await initialize();
    var diagnostics = await diagnosticsUpdate;
    var diagnostic = diagnostics!.singleWhere(
      (diagnostic) => diagnostic.code == 'ambiguous_extension_member_access',
    );

    expect(diagnostic.relatedInformation, hasLength(2));
    var [related1, related2] = diagnostic.relatedInformation!;
    expect(related1.message, contains('<unnamed extension> is defined in'));
    expect(related1.location.range, code.ranges[0].range);
    expect(related2.message, contains('<unnamed extension> is defined in'));
    expect(related2.location.range, code.ranges[1].range);
  }

  Future<void> test_correction() async {
    newFile(mainFilePath, '''
void f() {
  x = 0;
}
''');

    var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await initialize();
    var diagnostics = await diagnosticsUpdate;
    expect(diagnostics, hasLength(1));
    var diagnostic = diagnostics!.first;
    expect(diagnostic.message, contains('\nTry'));
  }

  /// Verify that if a nonexistant file is imported, creating that file causes
  /// the uri_does_not_exist error to go away.
  ///
  /// https://github.com/dart-lang/sdk/issues/55318
  Future<void> test_createAfterImport() async {
    var fooFilePath = join(projectFolderPath, 'lib', 'foo.dart');

    await initialize();
    await openFile(mainFileUri, "import 'foo.dart';");
    await pumpEventQueue(times: 5000);

    expect(diagnostics[mainFileUri]!.single.code, 'uri_does_not_exist');

    // Create the file.
    await openFile(toUri(fooFilePath), '');

    // Allow server to catch up.
    await pumpEventQueue(times: 5000);

    // Expect the diagnostic is now for an unused import instead.
    expect(diagnostics[mainFileUri]!.single.code, 'unused_import');
  }

  Future<void> test_deletedFile() async {
    newFile(mainFilePath, 'String a = 1;');

    var firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await initialize();
    var originalDiagnostics = await firstDiagnosticsUpdate;
    expect(originalDiagnostics, hasLength(1));

    // Deleting the file should result in an update to remove the diagnostics.
    var secondDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
    deleteFile(mainFilePath);
    var updatedDiagnostics = await secondDiagnosticsUpdate;
    expect(updatedDiagnostics, hasLength(0));
  }

  Future<void> test_diagnosticTag_deprecated() async {
    setDiagnosticTagSupport([DiagnosticTag.Deprecated]);

    var onePackagePath = convertPath('/home/one');
    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'one', rootFolder: getFolder(onePackagePath)),
    );
    newFile(convertPath('$onePackagePath/lib/one.dart'), '''
    @deprecated
    int? dep;
    ''');
    newFile(mainFilePath, r'''
    import 'package:one/one.dart';
    void f() => print(dep);
    ''');

    var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await initialize();
    var diagnostics = await diagnosticsUpdate;
    expect(diagnostics, hasLength(1));
    var diagnostic = diagnostics!.first;
    expect(diagnostic.code, equals('deprecated_member_use'));
    expect(diagnostic.tags, contains(DiagnosticTag.Deprecated));
  }

  Future<void> test_diagnosticTag_notSupported() async {
    var onePackagePath = convertPath('/home/one');
    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'one', rootFolder: getFolder(onePackagePath)),
    );
    newFile(convertPath('$onePackagePath/lib/one.dart'), '''
    @deprecated
    int? dep;
    ''');
    newFile(mainFilePath, r'''
    import 'package:one/one.dart';
    void f() => print(dep);
    ''');

    var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await initialize();
    var diagnostics = await diagnosticsUpdate;
    expect(diagnostics, hasLength(1));
    var diagnostic = diagnostics!.first;
    expect(diagnostic.code, equals('deprecated_member_use'));
    expect(diagnostic.tags, isNull);
  }

  Future<void> test_diagnosticTag_unnecessary() async {
    setDiagnosticTagSupport([DiagnosticTag.Unnecessary]);

    newFile(mainFilePath, '''
    void f() {
      return;
      print('unreachable');
    }
    ''');

    var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await initialize();
    var diagnostics = await diagnosticsUpdate;
    expect(diagnostics, hasLength(1));
    var diagnostic = diagnostics!.first;
    expect(diagnostic.code, equals('dead_code'));
    expect(diagnostic.tags, contains(DiagnosticTag.Unnecessary));
  }

  Future<void> test_documentationUrl() async {
    setDiagnosticCodeDescriptionSupport();

    newFile(mainFilePath, '''
    // ignore: unused_import
    import 'dart:async' as import; // produces BUILT_IN_IDENTIFIER_IN_DECLARATION
    ''');

    var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await initialize();
    var diagnostics = await diagnosticsUpdate;
    expect(diagnostics, hasLength(1));
    var diagnostic = diagnostics!.first;
    expect(diagnostic.code, equals('built_in_identifier_in_declaration'));
    expect(
      diagnostic.codeDescription!.href,
      equals(
        Uri.parse(
          'https://dart.dev/diagnostics/built_in_identifier_in_declaration',
        ),
      ),
    );
  }

  Future<void> test_documentationUrl_notSupported() async {
    newFile(mainFilePath, '''
    // ignore: unused_import
    import 'dart:async' as import; // produces BUILT_IN_IDENTIFIER_IN_DECLARATION
    ''');

    var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await initialize();
    var diagnostics = await diagnosticsUpdate;
    expect(diagnostics, hasLength(1));
    var diagnostic = diagnostics!.first;
    expect(diagnostic.code, equals('built_in_identifier_in_declaration'));
    expect(diagnostic.codeDescription, isNull);
  }

  Future<void> test_dotFilesExcluded() async {
    var dotFolderFilePath = join(
      projectFolderPath,
      '.dart_tool',
      'tool_file.dart',
    );
    var dotFolderFileUri = pathContext.toUri(dotFolderFilePath);

    newFile(dotFolderFilePath, 'String a = 1;');

    List<Diagnostic>? diagnostics;
    // Record if diagnostics are received, but since we don't expect them
    // don't await them.
    unawaited(
      waitForDiagnostics(dotFolderFileUri).then((d) => diagnostics = d),
    );

    // Send a request for a hover.
    await initialize();
    await getHover(dotFolderFileUri, Position(line: 0, character: 0));
    await pumpEventQueue(times: 5000);

    // Ensure that as part of responding to getHover, diagnostics were not
    // transmitted.
    expect(diagnostics, isNull);
  }

  /// Verify we don't send a redundant set of empty diagnostics during startup.
  Future<void> test_emptyDiagnostics_notInitial() async {
    newFile(mainFilePath, 'void f() {}');

    var notifications = <PublishDiagnosticsParams>[];
    publishedDiagnostics.listen(notifications.add);

    await initialize();
    await pumpEventQueue(times: 5000);

    expect(notifications.length, isZero);
  }

  /// Verify we only send diagnostic updates when a) they're not empty or
  /// b) they're empty, but the previous set was not empty.
  Future<void> test_emptyDiagnostics_onlyOnce() async {
    var notifications = <PublishDiagnosticsParams>[];
    publishedDiagnostics.listen(notifications.add);

    await initialize();
    await pumpEventQueue(times: 5000);

    /// Helper that executes [f] and checks whether it produces a diagnostic
    /// update.
    Future<void> verifyAction(_VoidCallback f, bool expectUpdate) async {
      notifications.clear();
      await f();
      await pumpEventQueue(times: 5000);
      expect(notifications, hasLength(expectUpdate ? 1 : 0));
    }

    Future<void> expectUpdate(_VoidCallback f) => verifyAction(f, true);
    Future<void> expectNoUpdate(_VoidCallback f) => verifyAction(f, false);

    // New file, 0 diagnostics, sends no notification
    await expectNoUpdate(() => openFile(mainFileUri, 'void f() {}'));
    // Update, non-empty diagnostic always sends notification
    await expectUpdate(() => replaceFile(1, mainFileUri, 'v'));
    // Update, non-empty diagnostic always sends notification
    await expectUpdate(() => replaceFile(1, mainFileUri, 'g'));
    // Update, 1->0 diagnostics, sends notification
    await expectUpdate(() => replaceFile(2, mainFileUri, 'void g() {}'));
    // Update, 0->0 diagnostics, sends no notification
    await expectNoUpdate(() => replaceFile(3, mainFileUri, 'void h() {}'));
  }

  /// Verify that computation of the QuickFix for "Create file 'Foo.dart'" does
  /// not trigger lint warnings for that file because the CreateFile fix tries
  /// to get a resolved unit which triggers analysis.
  ///
  /// https://github.com/Dart-Code/Dart-Code/issues/5343
  Future<void> test_exportMissingFile_noDiagnosticInMissingFile() async {
    registerLintRules();
    registerBuiltInAssistGenerators();
    registerBuiltInFixGenerators();
    setSupportedCodeActionKinds([CodeActionKind.QuickFix]);
    newFile(mainFilePath, "export 'Danny.dart';");
    newFile('$testPackageRootPath/lib/analysis_options.yaml', '''
linter:
  rules:
    - file_names
''');

    await initialize();
    var pos = Position(line: 0, character: 0);
    await getCodeActions(mainFileUri, position: pos);
    await pumpEventQueue(times: 5000);

    // We don't expect the lint diagnostic for the filename Danny.dart because
    // it doesn't exist.
    expect(diagnostics.keys, [mainFileUri]);
  }

  Future<void> test_fixDataFile() async {
    var fixDataPath = join(projectFolderPath, 'lib', 'fix_data.yaml');
    var fixDataUri = pathContext.toUri(fixDataPath);
    newFile(fixDataPath, '''
version: latest
''').path;

    var firstDiagnosticsUpdate = waitForDiagnostics(fixDataUri);
    await initialize();
    var initialDiagnostics = await firstDiagnosticsUpdate;
    expect(initialDiagnostics, hasLength(1));
    expect(initialDiagnostics!.first.severity, DiagnosticSeverity.Error);
    expect(initialDiagnostics.first.code, 'invalid_value');
  }

  Future<void> test_fromPlugins_dartFile() async {
    await checkPluginErrorsForFile(mainFilePath);
  }

  Future<void> test_fromPlugins_dartFile_combined() async {
    // Check that if code has both a plugin and a server error, that when the
    // plugin produces an error, it comes through _with_ the server-produced
    // error.
    // https://github.com/dart-lang/sdk/issues/45678
    //
    var serverErrorMessage =
        "A value of type 'int' can't be assigned to a variable of type 'String'";
    var pluginErrorMessage = 'Test error from plugin';

    newFile(mainFilePath, 'String a = 1;');
    var initialDiagnosticsFuture = waitForDiagnostics(mainFileUri);
    await initialize();
    var initialDiagnostics = await initialDiagnosticsFuture;
    expect(initialDiagnostics, hasLength(1));
    expect(initialDiagnostics!.first.message, contains(serverErrorMessage));

    var pluginTriggeredDiagnosticFuture = waitForDiagnostics(mainFileUri);
    var pluginError = plugin.AnalysisError(
      plugin.AnalysisErrorSeverity.ERROR,
      plugin.AnalysisErrorType.STATIC_TYPE_WARNING,
      plugin.Location(mainFilePath, 0, 1, 0, 0, endLine: 0, endColumn: 1),
      pluginErrorMessage,
      'ERR1',
    );
    var pluginResult = plugin.AnalysisErrorsParams(mainFilePath, [pluginError]);
    configureTestPlugin(notification: pluginResult.toNotification());

    var pluginTriggeredDiagnostics = await pluginTriggeredDiagnosticFuture;
    expect(
      pluginTriggeredDiagnostics!.map((error) => error.message),
      containsAll([pluginErrorMessage, contains(serverErrorMessage)]),
    );
  }

  /// Test that when server has produced diagnostics for a file and it is
  /// subsequently removed, that an update from the plugin does not cause
  /// the last diagnostics from the server to re-appear (which happens if the
  /// deletion does not clear the servers errors from the ErrorCollector).
  ///
  /// https://github.com/Dart-Code/Dart-Code/issues/5113
  Future<void> test_fromPlugins_dartFile_producedAfterFileRemoved() async {
    var serverErrorMessage =
        "A value of type 'int' can't be assigned to a variable of type 'String'";
    var pluginErrorMessage = 'Test error from plugin';

    // First, trigger a diagnostic from the server.
    newFile(mainFilePath, 'String a = 1;');
    var diagnosticsFuture = waitForDiagnostics(mainFileUri);
    await initialize();

    // Expect only the server diagnostic.
    expect(
      (await diagnosticsFuture)!.single.message,
      contains(serverErrorMessage),
    );

    // Delete the file, and expect diagnostics to be cleared.
    diagnosticsFuture = waitForDiagnostics(mainFileUri);
    deleteFile(mainFilePath);
    expect((await diagnosticsFuture)!, hasLength(0));

    // Trigger a plugin diagnostic. In reality, the plugin would probalby
    // produce 0 diagnostics after the file is removed, but since the LSP server
    // has an optimization to not send empty diagnostics if the last update was
    // empty, we wouldn't be able wait for that so we just use a real diagnostic
    // (which is still realistic since plugins might have not processed the
    // remove yet).
    diagnosticsFuture = waitForDiagnostics(mainFileUri);
    var pluginError = plugin.AnalysisError(
      plugin.AnalysisErrorSeverity.ERROR,
      plugin.AnalysisErrorType.STATIC_TYPE_WARNING,
      plugin.Location(mainFilePath, 0, 1, 0, 0, endLine: 0, endColumn: 1),
      pluginErrorMessage,
      'ERR1',
    );
    var pluginResult = plugin.AnalysisErrorsParams(mainFilePath, [pluginError]);
    configureTestPlugin(notification: pluginResult.toNotification());

    // Wait for the diagnostic updated and ensure it's still empty and no stale
    // error has come back.
    expect((await diagnosticsFuture)!.single.message, pluginErrorMessage);
  }

  Future<void> test_fromPlugins_nonDartFile() async {
    await checkPluginErrorsForFile(join(projectFolderPath, 'lib', 'foo.sql'));
  }

  Future<void> test_initialAnalysis() async {
    newFile(mainFilePath, 'String a = 1;');

    var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await initialize();
    var diagnostics = await diagnosticsUpdate;
    expect(diagnostics, hasLength(1));
    var diagnostic = diagnostics!.first;
    expect(diagnostic.code, equals('invalid_assignment'));
    expect(diagnostic.range.start.line, equals(0));
    expect(diagnostic.range.start.character, equals(11));
    expect(diagnostic.range.end.line, equals(0));
    expect(diagnostic.range.end.character, equals(12));
  }

  /// Ensure lints included from another package work when there are multiple
  /// workspace folders.
  Future<void> test_lints_includedFromPackage() async {
    var rootWorkspacePath = '$packagesRootPath/root';

    // Set up a project with an analysis_options that enables a lint.
    var lintsPackagePath = '$rootWorkspacePath/my_lints';
    newFile('$lintsPackagePath/pubspec.yaml', '''
name: my_lints
''');
    newFile('$lintsPackagePath/lib/analysis_options.yaml', '''
linter:
  rules:
    - avoid_dynamic_calls
''');
    writePackageConfig(convertPath(lintsPackagePath));

    // Set up a project that imports the analysis_options and violates the lint.
    var projectPackagePath = '$rootWorkspacePath/my_project';
    writePackageConfig(
      projectPackagePath,
      config: PackageConfigFileBuilder()
        ..add(name: 'my_lints', rootFolder: getFolder(lintsPackagePath)),
    );
    newFile('$projectPackagePath/analysis_options.yaml', '''
include: package:my_lints/analysis_options.yaml

linter:
  rules:
    - prefer_single_quotes
''');
    newFile('$projectPackagePath/main.dart', '''
void f(dynamic a) => a.foo();
''');

    // Verify there's an error for the import.
    var diagnosticsUpdate = waitForDiagnostics(
      toUri('$projectPackagePath/main.dart'),
    );
    await initialize(workspaceFolders: [toUri(rootWorkspacePath)]);
    var diagnostics = await diagnosticsUpdate;
    expect(diagnostics!.single.code, contains('avoid_dynamic_calls'));
  }

  Future<void> test_looseFile_withoutPubpsec() async {
    await initialize(allowEmptyRootUri: true);

    // Opening the file should trigger diagnostics.
    {
      var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
      await openFile(mainFileUri, 'final a = Bad();');
      var diagnostics = await diagnosticsUpdate;
      expect(diagnostics, hasLength(1));
      var diagnostic = diagnostics!.first;
      expect(diagnostic.message, contains("The function 'Bad' isn't defined"));
    }

    // Closing the file should remove the diagnostics.
    {
      var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
      await closeFile(mainFileUri);
      var diagnostics = await diagnosticsUpdate;
      expect(diagnostics, hasLength(0));
    }
  }

  /// Verify that when a file is renamed, if references to the new file are
  /// written into the documents before the new file is "created", it does not
  /// result in uri_does_not_exist errors that hang around.
  ///
  /// https://github.com/Dart-Code/Dart-Code/issues/5269
  Future<void> test_rename_updatedImportsBeforeCreated() async {
    var oldFilePath = join(projectFolderPath, 'lib', 'old.dart');
    var newFilePath = join(projectFolderPath, 'lib', 'new.dart');
    newFile(oldFilePath, '');
    newFile(mainFilePath, '');

    await initialize();
    await pumpEventQueue(times: 5000);

    // Create a valid reference to the old file.
    await openFile(mainFileUri, "import 'old.dart';");
    await openFile(toUri(oldFilePath), '');
    await pumpEventQueue(times: 5000);

    // Update the reference to the new file, update the overlays, and update
    // the disk.
    await replaceFile(2, mainFileUri, "import 'new.dart';");
    await closeFile(toUri(oldFilePath));
    await openFile(toUri(newFilePath), '');
    deleteFile(oldFilePath);
    newFile(newFilePath, '');

    // Allow server to catch up.
    await waitForAnalysisComplete();

    // Expect unused_import, not uri_does_not_exist.
    expect(diagnostics[mainFileUri]!.single.code, 'unused_import');
  }

  /// Tests that diagnostic ordering is stable when minor changes are made to
  /// the file that does not alter the diagnostics besides extending their
  /// range and adding to their messages.
  ///
  /// https://github.com/Dart-Code/Dart-Code/issues/3934
  Future<void> test_stableOrder() async {
    /// Helper to pad out the content in a way that has previously triggered
    /// this issue.
    String wrappedContent(String content) =>
        '''
//
//
//
//

void f() {
  $content
}
''';

    newFile(analysisOptionsPath, '''
linter:
  rules:
    - prefer_typing_uninitialized_variables

analyzer:
  language:
    strict-inference: true
    ''');

    newFile(mainFilePath, '');
    await initialize();
    await openFile(mainFileUri, '');

    // Collect the initial set of diagnostic to compare against.
    var docVersion = 1;
    var originalDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
    await replaceFile(docVersion++, mainFileUri, wrappedContent('final bar;'));
    var originalDiagnostics = await originalDiagnosticsUpdate;

    // Helper to update the content and verify the same diagnostics are returned
    // in the same order, despite the changes to offset/message altering
    // hashcodes.
    Future<void> verifyDiagnostics(String content) async {
      var diagnosticsUpdate = waitForDiagnostics(mainFileUri);
      await replaceFile(docVersion++, mainFileUri, wrappedContent(content));
      var diagnostics = await diagnosticsUpdate;
      expect(
        diagnostics!.map((d) => d.code),
        originalDiagnostics!.map((d) => d.code),
      );
    }

    // These changes do not affect the errors being produced (besides offset/
    // message text) but will cause hashcode changes that previously altered the
    // returned order.
    await verifyDiagnostics('final dbar;');
    await verifyDiagnostics('final dybar;');
    await verifyDiagnostics('final dynbar;');
    await verifyDiagnostics('final dynabar;');
    await verifyDiagnostics('final dynambar;');
    await verifyDiagnostics('final dynamibar;');
    await verifyDiagnostics('final dynamicbar;');
  }

  Future<void> test_todos_asWarnings() async {
    newFile(analysisOptionsPath, '''
analyzer:
  errors:
    # Increase the severity of TODOs.
    todo: warning
    fixme: warning
''');

    const contents = '''
    // TODO: This
    // FIXME: This
    String a = "";
    ''';
    newFile(mainFilePath, contents);

    var firstDiagnosticsUpdate = waitForDiagnostics(mainFileUri);
    // Don't set showTodos in config, because they should show even without this
    // setting if they are upgraded to warnings/errors.
    await initialize();
    var initialDiagnostics = await firstDiagnosticsUpdate;
    expect(initialDiagnostics, hasLength(2));
    expect(initialDiagnostics!.map((d) => d.code).toSet(), {'todo', 'fixme'});
  }

  Future<void> test_todos_boolean() async {
    // TODOs only show up if there's also some code in the file.
    const contents = '''
    // TODO: This
    // FIXME: This
    String a = "";
    ''';
    newFile(mainFilePath, contents);

    await provideConfig(initialize, {'showTodos': true});
    expect(diagnostics[mainFileUri], hasLength(2));
  }

  Future<void> test_todos_disabled() async {
    const contents = '''
    // TODO: This
    String a = "";
    ''';
    newFile(mainFilePath, contents);

    // TODOs are disabled by default so we don't need to send any config.
    await initialize();
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNull);
  }

  Future<void> test_todos_enabledAfterAnalysis() async {
    const contents = '''
    // TODO: This
    String a = "";
    ''';

    await provideConfig(initialize, {});
    await openFile(mainFileUri, contents);
    await initialAnalysis;
    expect(diagnostics[mainFileUri], isNull);

    await Future.wait([
      updateConfig({'showTodos': true}),
      waitForAnalysisComplete(),
    ]);
    expect(diagnostics[mainFileUri], hasLength(1));
  }

  Future<void> test_todos_specific() async {
    // TODOs only show up if there's also some code in the file.
    const contents = '''
    // TODO: This
    // HACK: This
    // FIXME: This
    String a = "";
    ''';
    newFile(mainFilePath, contents);

    await provideConfig(initialize, {
      // Include both casings, since this comes from the user we should handle
      // either.
      'showTodos': ['TODO', 'fixme'],
    });
    await initialAnalysis;

    var initialDiagnostics = diagnostics[mainFileUri]!;
    expect(initialDiagnostics, hasLength(2));
    expect(
      initialDiagnostics.map((e) => e.code!),
      containsAll(['todo', 'fixme']),
    );
  }
}
