// 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/services/correction/fix_internal.dart';
import 'package:linter/src/rules.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../shared/shared_code_actions_source_tests.dart';
import 'code_actions_mixin.dart';
import 'server_abstract.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(SourceCodeActionsTest);
    defineReflectiveTests(SortMembersSourceCodeActionsTest);
    defineReflectiveTests(OrganizeImportsSourceCodeActionsTest);
    defineReflectiveTests(FixAllSourceCodeActionsTest);
  });
}

abstract class AbstractSourceCodeActionsTest
    extends AbstractLspAnalysisServerTest
    with
        LspSharedTestMixin,
        SharedSourceCodeActionsTestMixin,
        CodeActionsTestMixin {}

@reflectiveTest
class FixAllSourceCodeActionsTest extends AbstractSourceCodeActionsTest {
  @override
  Future<void> setUp() async {
    await super.setUp();

    // Fix tests are likely to have diagnostics that need fixing.
    failTestOnErrorDiagnostic = false;

    registerBuiltInFixGenerators();
  }

  Future<void> test_appliesCorrectEdits() async {
    const analysisOptionsContent = '''
linter:
  rules:
    - unnecessary_new
    - prefer_collection_literals
''';
    const content = '''
final a = new Object();
final b = new Set<String>();
''';
    const expectedContent = '''
final a = Object();
final b = <String>{};
''';

    registerLintRules();
    newFile(analysisOptionsPath, analysisOptionsContent);
    newFile(mainFilePath, content);

    await verifyCodeActionLiteralEdits(
      content,
      expectedContent,
      command: Commands.fixAll,
    );
  }

  Future<void> test_multipleIterations_noOverlay() async {
    const analysisOptionsContent = '''
linter:
  rules:
    - prefer_final_locals
    - prefer_const_declarations
    ''';
    const content = '''
void f() {
  var a = 'test';
}
''';
    const expectedContent = '''
void f() {
  const a = 'test';
}
''';

    registerLintRules();
    newFile(analysisOptionsPath, analysisOptionsContent);
    newFile(mainFilePath, content);

    await verifyCodeActionLiteralEdits(
      content,
      expectedContent,
      command: Commands.fixAll,
    );
  }

  Future<void> test_multipleIterations_overlay() async {
    const analysisOptionsContent = '''
linter:
  rules:
    - prefer_final_locals
    - prefer_const_declarations
    ''';
    const content = '''
void f() {
  var a = 'test';
}
''';
    const expectedContent = '''
void f() {
  const a = 'test';
}
''';

    registerLintRules();
    newFile(analysisOptionsPath, analysisOptionsContent);

    await verifyCodeActionLiteralEdits(
      content,
      expectedContent,
      command: Commands.fixAll,
    );
  }

  Future<void> test_multipleIterations_withClientModification() async {
    const analysisOptionsContent = '''
linter:
  rules:
    - prefer_final_locals
    - prefer_const_declarations
    ''';
    const content = '''
void f() {
  var a = 'test';
}
''';
    registerLintRules();
    newFile(analysisOptionsPath, analysisOptionsContent);

    var codeAction = await expectCodeActionLiteral(
      content,
      command: Commands.fixAll,
    );
    var command = codeAction.command!;

    // Files must be open to apply edits.
    await openFile(testFileUri, content);

    // Execute the command with a modification and capture the edit that is
    // sent back to us.
    ApplyWorkspaceEditParams? editParams;
    await handleExpectedRequest<
      Object?,
      ApplyWorkspaceEditParams,
      ApplyWorkspaceEditResult
    >(
      Method.workspace_applyEdit,
      ApplyWorkspaceEditParams.fromJson,
      () async {
        // Apply the command and immediately modify a file afterwards without
        // awaiting.
        var commandFuture = executeCommand(command);
        await replaceFile(12345, testFileUri, 'client-modified-content');

        return commandFuture;
      },
      handler: (edit) async {
        // When the server sends the edit back, just keep a copy and say we
        // applied successfully (we'll verify the actual edit below).
        editParams = edit;
        return ApplyWorkspaceEditResult(applied: true);
      },
    );

    // Extract the text edit from the 'workspace/applyEdit' params the server
    // sent us.
    var change = editParams?.edit.documentChanges!.single;
    var edit = change!.map(
      (create) => throw 'Expected edit, got create',
      (delete) => throw 'Expected edit, got delete',
      (rename) => throw 'Expected edit, got rename',
      (edit) => edit,
    );
    // Ensure the edit says that it was based on version 1 (the original
    // version) and not the updated 12345 version we sent.
    expect(edit.textDocument.version, 1);
  }

  Future<void> test_part() async {
    var containerFilePath = join(projectFolderPath, 'lib', 'container.dart');
    var partFilePath = join(projectFolderPath, 'lib', 'part.dart');
    const analysisOptionsContent = '''
linter:
  rules:
    - unnecessary_new
    - prefer_collection_literals
''';
    const containerFileContent = '''
part 'part.dart';
''';
    const content = '''
part of 'container.dart';

final a = new Object();
final b = new Set<String>();
''';
    const expectedContent = '''
part of 'container.dart';

final a = Object();
final b = <String>{};
''';

    registerLintRules();
    newFile(analysisOptionsPath, analysisOptionsContent);
    newFile(containerFilePath, containerFileContent);
    newFile(partFilePath, content);

    await verifyCodeActionLiteralEdits(
      filePath: partFilePath,
      content,
      expectedContent,
      command: Commands.fixAll,
    );
  }

  Future<void> test_privateUnusedParameters_notRemovedIfSave() async {
    const content = '''
class _MyClass {
  int? _param;
  _MyClass({
    this._param,
  });
}
''';

    var codeAction = await expectCodeActionLiteral(
      content,
      command: Commands.fixAll,
      triggerKind: CodeActionTriggerKind.Automatic,
    );
    var command = codeAction.command!;

    // We should not get an applyEdit call during the command execution because
    // no edits should be produced.
    var applyEditSubscription = requestsFromServer
        .where((n) => n.method == Method.workspace_applyEdit)
        .listen((_) => throw 'workspace/applyEdit was unexpectedly called');
    var commandResponse = await executeCommand(command);
    expect(commandResponse, isNull);

    await pumpEventQueue();
    await applyEditSubscription.cancel();
  }

  Future<void> test_privateUnusedParameters_removedByDefault() async {
    const content = '''
class _MyClass {
  int? param;
  _MyClass({
    this.param,
  });
}
''';
    const expectedContent = '''
class _MyClass {
  int? param;
  _MyClass() : param = null;
}
''';

    await verifyCodeActionLiteralEdits(
      content,
      expectedContent,
      command: Commands.fixAll,
    );
  }

  Future<void> test_unavailable_outsideAnalysisRoot() async {
    var otherFile = convertPath('/other/file.dart');
    var content = '';

    await expectNoAction(
      filePath: otherFile,
      content,
      command: Commands.organizeImports,
    );
  }

  Future<void> test_unusedUsings_notRemovedIfSave() async {
    const content = '''
import 'dart:async';
int? a;
''';

    var codeAction = await expectCodeActionLiteral(
      content,
      command: Commands.fixAll,
      triggerKind: CodeActionTriggerKind.Automatic,
    );
    var command = codeAction.command!;

    // We should not get an applyEdit call during the command execution because
    // no edits should be produced.
    var applyEditSubscription = requestsFromServer
        .where((n) => n.method == Method.workspace_applyEdit)
        .listen((_) => throw 'workspace/applyEdit was unexpectedly called');
    var commandResponse = await executeCommand(command);
    expect(commandResponse, isNull);

    await pumpEventQueue();
    await applyEditSubscription.cancel();
  }

  Future<void> test_unusedUsings_removedByDefault() async {
    const content = '''
import 'dart:async';
int? a;
''';
    const expectedContent = '''
int? a;
''';

    await verifyCodeActionLiteralEdits(
      content,
      expectedContent,
      command: Commands.fixAll,
    );
  }
}

@reflectiveTest
class OrganizeImportsSourceCodeActionsTest extends AbstractSourceCodeActionsTest
    with
        SharedSourceCodeActionsTestMixin,
        // Tests are defined in a shared mixin.
        SharedOrganizeImportsSourceCodeActionsTests {}

@reflectiveTest
class SortMembersSourceCodeActionsTest extends AbstractSourceCodeActionsTest
    with
        SharedSourceCodeActionsTestMixin,
        // Tests are defined in a shared mixin.
        SharedSortMembersSourceCodeActionsTests {}

@reflectiveTest
class SourceCodeActionsTest extends AbstractSourceCodeActionsTest
    with
        SharedSourceCodeActionsTestMixin,
        // Tests are defined in a shared mixin.
        SharedSourceCodeActionsTests {}
