// Copyright (c) 2019, 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:analyzer_plugin/protocol/protocol_common.dart' hide Position;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import 'server_abstract.dart';

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

@reflectiveTest
class DocumentChangesTest extends AbstractLspAnalysisServerTest {
  String get content => '''
class Foo {
  String get bar => 'baz';
}
''';

  String get contentAfterUpdate => '''
class Bar {
  String get bar => 'updated';
}
''';

  Future<void> test_documentChange_notifiesPlugins() async {
    await _initializeAndOpen();
    await changeFile(2, mainFileUri, [
      TextDocumentContentChangeEvent.t1(
        TextDocumentContentChangePartial(
          range: Range(
            start: Position(line: 0, character: 6),
            end: Position(line: 0, character: 9),
          ),
          text: 'Bar',
        ),
      ),
      TextDocumentContentChangeEvent.t1(
        TextDocumentContentChangePartial(
          range: Range(
            start: Position(line: 1, character: 21),
            end: Position(line: 1, character: 24),
          ),
          text: 'updated',
        ),
      ),
    ]);

    var notifiedChanges =
        pluginManager.analysisUpdateContentParams!.files[mainFilePath]
            as ChangeContentOverlay;

    expect(
      SourceEdit.applySequence(content, notifiedChanges.edits),
      contentAfterUpdate,
    );
  }

  Future<void> test_documentChange_updatesOverlay() async {
    await _initializeAndOpen();
    await changeFile(2, mainFileUri, [
      TextDocumentContentChangeEvent.t1(
        TextDocumentContentChangePartial(
          range: Range(
            start: Position(line: 0, character: 6),
            end: Position(line: 0, character: 9),
          ),
          text: 'Bar',
        ),
      ),
      TextDocumentContentChangeEvent.t1(
        TextDocumentContentChangePartial(
          range: Range(
            start: Position(line: 1, character: 21),
            end: Position(line: 1, character: 24),
          ),
          text: 'updated',
        ),
      ),
    ]);

    expect(server.resourceProvider.hasOverlay(mainFilePath), isTrue);
    expect(
      server.resourceProvider.getFile(mainFilePath).readAsStringSync(),
      equals(contentAfterUpdate),
    );
  }

  Future<void> test_documentClose_deletesOverlay() async {
    await _initializeAndOpen();
    await closeFile(mainFileUri);

    expect(server.resourceProvider.hasOverlay(mainFilePath), isFalse);
  }

  Future<void> test_documentClose_notifiesPlugins() async {
    await _initializeAndOpen();
    await closeFile(mainFileUri);

    expect(
      pluginManager.analysisUpdateContentParams!.files,
      equals({mainFilePath: RemoveContentOverlay()}),
    );
  }

  Future<void>
  test_documentOpen_addsOverlayOnlyToDriver_onlyIfInsideRoots() async {
    // Ensures that opening a file doesn't add it to the driver if it's outside
    // of the drivers root.
    var fileInsideRootPath = mainFilePath;
    var fileOutsideRootPath = convertPath('/home/unrelated/main.dart');
    await initialize();
    await openFile(pathContext.toUri(fileInsideRootPath), content);
    await openFile(pathContext.toUri(fileOutsideRootPath), content);

    // Expect both files return the same driver
    var driverForInside = server.getAnalysisDriver(fileInsideRootPath)!;
    var driverForOutside = server.getAnalysisDriver(fileOutsideRootPath)!;
    expect(driverForInside, equals(driverForOutside));
    // But that only the file inside the root was added.
    expect(driverForInside.addedFiles, contains(fileInsideRootPath));
    expect(driverForInside.addedFiles, isNot(contains(fileOutsideRootPath)));
  }

  Future<void> test_documentOpen_contentChanged_analysis() async {
    failTestOnErrorDiagnostic = false;

    const content = '// original content';
    const newContent = 'new content'; // triggers diagnostic
    newFile(mainFilePath, content);

    await initialize();
    await initialAnalysis;

    await openFile(mainFileUri, newContent);
    await pumpEventQueue(times: 50000);

    // Expect diagnostics, because changing the content will have triggered
    // analysis.
    expect(diagnostics[mainFileUri], isNotEmpty);
  }

  Future<void> test_documentOpen_contentUnchanged_noAnalysis() async {
    const content = '// original content';
    newFile(mainFilePath, content);

    await initialize();
    await initialAnalysis;

    await openFile(mainFileUri, content);
    await pumpEventQueue(times: 5000);

    // Expect no diagnostics because the file didn't actually change content
    // when the overlay was created, so it should not have triggered analysis.
    expect(diagnostics[mainFileUri], isNull);
  }

  Future<void> test_documentOpen_createsOverlay() async {
    await _initializeAndOpen();

    expect(server.resourceProvider.hasOverlay(mainFilePath), isTrue);
    expect(
      server.resourceProvider.getFile(mainFilePath).readAsStringSync(),
      equals(content),
    );
  }

  /// Verify that calling open/close repeatedly produces no errors and ends
  /// in the closed state.
  Future<void> test_documentOpen_documentClose_repeatedly_endClosed() async {
    await initialize(
      // This forces roots to be rebuilt when the files open/close which
      // increases the workload and makes any `await` in processing open/close
      // files more likely to trigger the failure.
      initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true},
    );

    await Future.wait([
      for (var i = 0; i < 100; i++) ...[
        openFile(mainFileUri, content),
        closeFile(mainFileUri),
      ],
    ]);

    expect(server.resourceProvider.hasOverlay(mainFilePath), isFalse);
  }

  /// Verify that calling open/close repeatedly produces no errors and ends
  /// in the open state if the last entry was open.
  Future<void> test_documentOpen_documentClose_repeatedly_endOpen() async {
    await initialize(
      // This forces roots to be rebuilt when the files open/close which
      // increases the workload and makes any `await` in processing open/close
      // files more likely to trigger the failure.
      initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true},
    );

    await Future.wait([
      for (var i = 0; i < 100; i++) ...[
        openFile(mainFileUri, content),
        closeFile(mainFileUri),
      ],
      openFile(mainFileUri, content),
    ]);
    await pumpEventQueue(times: 50000);
    expect(server.resourceProvider.hasOverlay(mainFilePath), isTrue);
  }

  /// Tests that deleting a file does not clear diagnostics while there's an
  /// overlay, and that removing the overlay later clears the diagnostics.
  ///
  /// https://github.com/dart-lang/sdk/issues/53475
  Future<void> test_documentOpen_fileDeleted_documentClosed() async {
    failTestOnErrorDiagnostic = false;

    const content = 'error';
    newFile(mainFilePath, content);

    // Expect diagnostics after initial analysis because file has invalid
    // content.
    await initialize();
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNotEmpty);

    // Expect diagnostics after opening the file with the same contents.
    await openFile(mainFileUri, content);
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNotEmpty);

    // Expect diagnostics after deleting the file because the overlay is still
    // active.
    deleteFile(mainFilePath);
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNotEmpty);

    // Expect diagnostics to be removed after we close the file (which removes
    // the overlay).
    await closeFile(mainFileUri);
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isEmpty);
  }

  /// 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_documentOpen_fileDeleted_fileCreated_documentClosed_fileDeleted() async {
    failTestOnErrorDiagnostic = false;

    const content = 'error';
    newFile(mainFilePath, content);

    // Expect diagnostics after initial analysis because file has invalid
    // content.
    await initialize();
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNotEmpty);

    // Expect diagnostics after opening the file with the same contents.
    await openFile(mainFileUri, content);
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNotEmpty);

    // Expect diagnostics after deleting the file because the overlay is still
    // active.
    deleteFile(mainFilePath);
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNotEmpty);

    // Expect diagnostics remain after re-creating the file (the overlay is still
    // active).
    newFile(mainFilePath, content);
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNotEmpty);

    // Expect diagnostics remain after we close the file because the file still
    //exists on disk.
    await closeFile(mainFileUri);
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isNotEmpty);

    // Finally, expect deleting the file clears the diagnostics.
    deleteFile(mainFilePath);
    await pumpEventQueue(times: 5000);
    expect(diagnostics[mainFileUri], isEmpty);
  }

  Future<void> test_documentOpen_notifiesPlugins() async {
    await _initializeAndOpen();

    expect(
      pluginManager.analysisUpdateContentParams!.files,
      equals({mainFilePath: AddContentOverlay(content)}),
    );
  }

  /// Verifies the fix for a race condition where an overlay would not be
  /// processed if the file was created on disk before the overlay was processed
  /// (but the watch event had also not yet been processed).
  ///
  /// https://github.com/dart-lang/sdk/issues/51159
  Future<void> test_documentOpen_processesOverlay_dartSdk_issue51159() async {
    failTestOnErrorDiagnostic = false;

    var binFolder = convertPath(join(projectFolderPath, 'bin'));
    var binMainFilePath = convertPath(join(binFolder, 'main.dart'));
    var binMainFileUri = pathContext.toUri(binMainFilePath);
    var fooFilePath = convertPath(join(binFolder, 'foo.dart'));
    var fooUri = pathContext.toUri(fooFilePath);

    const binMainContent = '''
import 'foo.dart';

Foo? f;
''';
    const fooContent = '''
class Foo {}
''';

    newFolder(binFolder);
    newFile(binMainFilePath, binMainContent);

    await initialize();
    await initialAnalysis;

    // Expect diagnostics because 'foo.dart' doesn't exist.
    expect(diagnostics[binMainFileUri], isNotEmpty);

    // Create the file and _immediately_ open it, so the file exists when the
    // overlay is created, even though the watcher event has not been processed.
    newFile(fooFilePath, fooContent);
    await Future.wait([
      openFile(fooUri, fooContent),
      waitForAnalysisComplete(),
    ]);

    // Expect the diagnostics have gone.
    expect(diagnostics[binMainFileUri], isEmpty);
  }

  Future<void> test_documentOpen_setsPriorityFileIfEarly() async {
    setConfigurationSupport();

    // When initializing with config support, the server will call back to the client
    // which can delay analysis roots being configured. This can result in files
    // being opened before analysis roots are set which has previously caused the
    // files not to be marked as priority on the created drivers.
    // https://github.com/Dart-Code/Dart-Code/issues/2438
    // https://github.com/dart-lang/sdk/issues/42994

    // Initialize the server, but delay providing the configuration until after
    // we've opened the file.
    var completer = Completer<void>();

    // Send the initialize request but do not await it.
    var initResponse = initialize();

    // When asked for config, delay the response until we have sent the openFile notification.
    var config = provideConfig(
      () => initResponse,
      completer.future.then((_) => {'dart.foo': false}),
    );

    // Wait for initialization to finish, open the file, then allow config to complete.
    await initResponse;
    await openFile(mainFileUri, content);
    completer.complete();
    await config;
    await pumpEventQueue(times: 5000);

    // Ensure the opened file is in the priority list.
    expect(
      server.getAnalysisDriver(mainFilePath)!.priorityFiles,
      equals([mainFilePath]),
    );
  }

  Future<void> _initializeAndOpen() async {
    await initialize();
    await openFile(mainFileUri, content);
  }
}
