// Copyright (c) 2020, 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 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/services/correction/bulk_fix_processor.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../../../../utils/test_instrumentation_service.dart';
import 'fix_processor.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(AdditionalEnabledCodesTest);
    defineReflectiveTests(HasFixesTest);
    defineReflectiveTests(ChangeMapTest);
    defineReflectiveTests(NoFixTest);
    defineReflectiveTests(PubspecFixTest);
  });
}

@reflectiveTest
class AdditionalEnabledCodesTest extends BulkFixProcessorTest {
  Future<void> test_additionalEnabledCodes_areApplied() async {
    // Deliberately DO NOT enable the lint in the options file
    createAnalysisOptionsFile(experiments: experiments, lints: []);

    await resolveTestCode('''
class A { }

var a = new A();
''');

    var analysisContext = contextFor(testFile);
    var changeWorkspace = await workspace;
    var processor = BulkFixProcessor(
      TestInstrumentationService(),
      changeWorkspace,
      byteStore: byteStore,
      additionalEnabledCodes: [LintNames.unnecessary_new],
    );

    await processor.fixErrors([analysisContext]);

    var errors = processor.changeMap.libraryMap[testFile.path]!;
    expect(errors, hasLength(1));
    expect(errors[LintNames.unnecessary_new], 1);
  }

  Future<void>
  test_additionalEnabledCodes_doesNotFilterOutOriginalLints() async {
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [LintNames.annotate_overrides],
    );

    await resolveTestCode('''
class A {
  void f() { }
}

class B extends A {
  void f() { }
}

var a = new A();
''');

    var analysisContext = contextFor(testFile);
    var changeWorkspace = await workspace;
    var processor = BulkFixProcessor(
      TestInstrumentationService(),
      changeWorkspace,
      byteStore: byteStore,
      additionalEnabledCodes: [LintNames.unnecessary_new],
    );

    await processor.fixErrors([analysisContext]);

    var errors = processor.changeMap.libraryMap[testFile.path]!;
    expect(errors, hasLength(2));
    expect(errors[LintNames.annotate_overrides], 1);
    expect(errors[LintNames.unnecessary_new], 1);
  }

  Future<void> test_additionalEnabledCodes_invalidCodesIgnored() async {
    createAnalysisOptionsFile(experiments: experiments, lints: []);

    await resolveTestCode('''
class A { }

var a = new A();
''');

    var analysisContext = contextFor(testFile);
    var changeWorkspace = await workspace;
    var processor = BulkFixProcessor(
      TestInstrumentationService(),
      changeWorkspace,
      byteStore: byteStore,
      additionalEnabledCodes: ['some_fake_lint_that_doesnt_exist'],
    );

    await processor.fixErrors([analysisContext]);

    // No fixes should be applied, and it shouldn't crash
    expect(processor.changeMap.hasFixes, isFalse);
  }

  Future<void>
  test_additionalEnabledCodes_originalContextRemainsUnchanged() async {
    createAnalysisOptionsFile(experiments: experiments, lints: []);

    await resolveTestCode('''
class A { }

var a = new A();
''');

    var analysisContext = contextFor(testFile);
    var changeWorkspace = await workspace;
    var processor = BulkFixProcessor(
      TestInstrumentationService(),
      changeWorkspace,
      byteStore: byteStore,
      additionalEnabledCodes: [LintNames.unnecessary_new],
    );

    await processor.fixErrors([analysisContext]);

    // Ensure the original context's options map still does not contain the lint
    var originalLints = analysisContext
        .getAnalysisOptionsForFile(testFile)
        .lintRules
        .map((rule) => rule.name)
        .toList();
    expect(originalLints, isNot(contains(LintNames.unnecessary_new)));
  }
}

@reflectiveTest
class ChangeMapTest extends BulkFixProcessorTest {
  Future<void> test_changeMap() async {
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [LintNames.annotate_overrides, LintNames.unnecessary_new],
    );

    await resolveTestCode('''
class A { }

var a = new A();
var aa = new A();
''');

    var processor = await computeFixes();
    var changeMap = processor.changeMap;
    var errors = changeMap.libraryMap[testFile.path]!;
    expect(errors, hasLength(1));
    expect(errors[LintNames.unnecessary_new], 2);
  }

  Future<void> test_changeMap_cancelled() async {
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [LintNames.unnecessary_new],
    );

    await resolveTestCode('''
class A { }

var a = new A();
''');

    var analysisContext = contextFor(testFile);
    var changeWorkspace = await workspace;
    var token = CancelableToken();
    var processor = BulkFixProcessor(
      TestInstrumentationService(),
      changeWorkspace,
      byteStore: byteStore,
      cancellationToken: token,
    );

    // Begin computing fixes, then immediately cancel.
    var fixErrorsFuture = processor.fixErrors([analysisContext]);
    token.cancel();

    // Wait for code to return and expect that we didn't compute any changes
    // (because we exited early).
    await fixErrorsFuture;
    expect(processor.changeMap.libraryMap, isEmpty);
  }
}

@reflectiveTest
class HasFixesTest extends BulkFixProcessorTest {
  Future<void> test_hasFixes() async {
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [LintNames.annotate_overrides, LintNames.unnecessary_new],
    );

    await resolveTestCode('''
class A { }

var a = new A();
''');

    expect(await computeHasFixes(), isTrue);
  }

  Future<void> test_hasFixes_in_part() async {
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [LintNames.unnecessary_new],
    );

    newFile('$testPackageLibPath/a.dart', '''
part of 'test.dart';

class A { }

var a = new A();
''');

    await resolveTestCode('''
part 'a.dart';
''');

    expect(await computeHasFixes(), isTrue);
  }

  Future<void> test_hasFixes_in_part_and_library() async {
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [LintNames.unnecessary_new],
    );

    newFile('$testPackageLibPath/c.dart', '''
part 'test.dart';
part 'b.dart';

class C{}

var c = C();
''');

    newFile('$testPackageLibPath/b.dart', '''
part of 'c.dart';

class B {}

var b = new B();
''');

    await resolveTestCode('''
part of 'c.dart';

class A {}

var a = new A();
''');

    expect(await computeHasFixes(), isTrue);
    processor = await computeFixes();
    expect(processor.changeMap.libraryMap, hasLength(2));
  }

  Future<void> test_hasFixes_in_part_and_library2() async {
    // https://github.com/dart-lang/sdk/issues/59572
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [LintNames.empty_statements, LintNames.prefer_const_constructors],
    );

    newFile('$testPackageLibPath/part.dart', '''
part of 'test.dart';

class C {
  const C();
}

C b() {
  // dart fix should only add a single const
  return C();
}
''');

    await resolveTestCode('''
part 'part.dart';

void a() {
  // need to trigger a lint in main.dart for the bug to happen
  ;
  b();
}
''');

    expect(await computeHasFixes(), isTrue);
    processor = await computeFixes();
    expect(processor.changeMap.libraryMap, hasLength(2));
    expect(processor.fixDetails.expand((d) => d.fixes), hasLength(2));
  }

  Future<void> test_hasFixes_stoppedAfterFirst() async {
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [LintNames.annotate_overrides, LintNames.unnecessary_new],
    );

    await resolveTestCode('''
class A { String a => ''; }
class B extends A { String a => ''; }

var a = new A();
''');

    expect(await computeHasFixes(), isTrue);
    // We should only have computed one, despite the above code having two
    // fixable issues.
    expect(processor.changeMap.libraryMap[testFile.path], hasLength(1));
  }

  Future<void> test_noFixes() async {
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [LintNames.avoid_catching_errors],
    );

    await resolveTestCode('''
void bad() {
  try {
  } on Error catch (e) {
    print(e);
  }
}
''');

    expect(await computeHasFixes(), isFalse);
  }

  Future<void> test_override_first() async {
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [
        LintNames.annotate_overrides,
        LintNames.always_declare_return_types,
      ],
    );
    await resolveTestCode('''
abstract class A {
  void foo();
}

class B extends A {
  foo() {}
}
''');

    expect(await computeHasFixes(), isTrue);
    processor = await computeFixes();
    expect(processor.fixDetails.single.fixes, hasLength(2));
    expect(processor.builder.sourceChange.edits.single.edits, hasLength(2));
  }
}

@reflectiveTest
class NoFixTest extends BulkFixProcessorTest {
  /// See: https://github.com/dart-lang/sdk/issues/45177
  Future<void> test_noFix() async {
    createAnalysisOptionsFile(
      experiments: experiments,
      lints: [LintNames.avoid_catching_errors],
    );

    await resolveTestCode('''
void bad() {
  try {
  } on Error catch (e) {
    print(e);
  }
}
''');

    var processor = await computeFixes();
    expect(processor.fixDetails, isEmpty);
  }
}

@reflectiveTest
class PubspecFixTest extends BulkFixProcessorTest {
  Future<void> test_dedupe_devPackages_against_packages() async {
    var content = '''
name: test
''';
    var expected = '''
name: test
dependencies:
  b: any
''';
    updateTestPubspecFile(content);

    newFile('$testPackageLibPath/lib.dart', '''
import 'package:b/b.dart';
''');

    var testFile = newFile('$testPackageTestPath/test.dart', '''
import 'package:b/b.dart';
''');

    await getResolvedUnit(testFile);
    await assertFixPubspec(content, expected);
  }

  Future<void> test_delete_change() async {
    var content = '''
name: test
dependencies:
  a: any
dev_dependencies:
  b: any
  c: any
  d: any
''';
    var expected = '''
name: test
dependencies:
  a: any
  c: any
dev_dependencies:
  b: any
  d: any
''';
    updateTestPubspecFile(content);

    newFile('$testPackageLibPath/lib.dart', '''
import 'package:c/c.dart';

void bad() {
  try {
  } on Error catch (e) {
    print(e);
  }
}
''');
    var testFile = newFile('$testPackageTestPath/test.dart', '''
import 'package:b/b.dart';
import 'package:c/c.dart';
import 'package:d/d.dart';
import 'package:test/lib.dart';

void f() {
  print(C());
}
''');

    await getResolvedUnit(testFile);
    await assertFixPubspec(content, expected);
  }

  Future<void> test_fix() async {
    var content = '''
name: test
''';
    var expected = '''
name: test
dependencies:
  a: any
''';
    updateTestPubspecFile(content);

    await resolveTestCode('''
import 'package:a/a.dart';

void bad() {
  try {
  } on Error catch (e) {
    print(e);
  }
}
''');
    await assertFixPubspec(content, expected);
  }

  Future<void> test_multiple_changes() async {
    var content = '''
name: test
dependencies:
  a: any
''';
    var expected = '''
name: test
dependencies:
  a: any
  b: any
  c: any
''';
    updateTestPubspecFile(content);

    await resolveTestCode('''
import 'package:b/b.dart';
import 'package:c/c.dart';

void bad() {
  try {
  } on Error catch (e) {
    print(e);
  }
}
''');

    await assertFixPubspec(content, expected);
  }

  Future<void> test_multiple_pubspec_change() async {
    var content = '''
name: test
dependencies:
  a: any
dev_dependencies:
  b: any
  d: any
  c: any
''';
    var expected = '''
name: test
dependencies:
  a: any
  c: any
  test2: any
dev_dependencies:
  b: any
  d: any
''';
    updateTestPubspecFile(content);

    newFile('$testPackageLibPath/lib.dart', '''
import 'package:c/c.dart';
import 'package:test2/lib.dart';
import 'package:flutter_gen/gen.dart';

void bad() {
  try {
  } on Error catch (e) {
    print(e);
  }
}
''');
    var testFile = newFile('$testPackageTestPath/test.dart', '''
import 'package:b/b.dart';
import 'package:c/c.dart';
import 'package:d/d.dart';
import 'package:test/lib.dart';

void f() {
  print(C());
}
''');

    newFile('$workspaceRootPath/test2/lib.dart', '''
import 'package:d/d.dart';
import 'package:flutter_gen/gen.dart';

class A{}
''');
    var test2PubspecContent = '''
name: test2
deps:
  d: any
''';
    var test2Pubspec = newFile(
      '$workspaceRootPath/test2/pubspec.yaml',
      test2PubspecContent,
    );
    await getResolvedUnit(testFile);
    await assertFixPubspec(content, expected);
    await assertFixPubspec(
      test2PubspecContent,
      test2PubspecContent,
      file: test2Pubspec,
    );
  }

  Future<void> test_no_exception() async {
    var content = '''
name: test
dependencies:
  a: any
  any
''';
    var expected = '''
name: test
dependencies:
  a: any
  any
''';

    updateTestPubspecFile(content);
    await resolveTestCode('''
import 'package:a/a.dart';

void bad() {
  try {
  } on Error catch (e) {
    print(e);
  }
}
''');

    await assertFixPubspec(content, expected);
  }

  Future<void> test_no_fix() async {
    var content = '''
name: test
dependencies:
  a: any
''';
    var expected = '''
name: test
dependencies:
  a: any
''';

    updateTestPubspecFile(content);
    await resolveTestCode('''
import 'package:a/a.dart';

void bad() {
  try {
  } on Error catch (e) {
    print(e);
  }
}
''');

    await assertFixPubspec(content, expected);
  }
}
