// Copyright (c) 2024, 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/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../../tool/lsp_spec/matchers.dart';
import '../../utils/lsp_protocol_extensions.dart';
import '../../utils/test_code_extensions.dart';
import '../server_abstract.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(AugmentationCodeLensTest);
    defineReflectiveTests(AugmentedCodeLensTest);
  });
}

@reflectiveTest
abstract class AbstractAugmentationCodeLensTest
    extends AbstractLspAnalysisServerTest {
  late TestCode libraryCode;
  late TestCode augmentationCode;

  /// The name of the setting to control whether [codeLensTitle] CodeLenses
  /// are enabled.
  ///
  /// ```js
  /// "dart.codeLens": {
  ///   "augmented": true
  ///    ^^^^^^^^^
  /// }
  /// ```
  String get codeLensSettingName;

  /// The title of the [Command] in the [CodeLens]es being tested.
  String get codeLensTitle;

  /// The range in [sourceUri] that the CodeLens should appear for.
  Range get sourceRange;

  /// The document for which the CodeLens request will be sent.
  Uri get sourceUri;

  /// The range in [targetUri] where the CodeLens should navigate to.
  Range get targetRange;

  /// The document that the CodeLens should navigate to.
  Uri get targetUri;

  /// Expects a CodeLens in [sourceUri] at [sourceRange] with the title
  /// [codeLensTitle] that navigates to [targetRange] in [targetUri].
  Future<void> expectNavigationCodeLens({
    Uri? sourceUri,
    Range? sourceRange,
    Uri? targetUri,
    Range? targetRange,
  }) async {
    sourceUri ??= this.sourceUri;
    sourceRange ??= this.sourceRange;
    targetUri ??= this.targetUri;
    targetRange ??= this.targetRange;

    var documentCodeLenses = (await getCodeLens(sourceUri)) ?? [];
    var matchingCodeLenses = documentCodeLenses.where(
      (codeLens) =>
          codeLens.command?.title == codeLensTitle &&
          codeLens.range.containsPosition(sourceRange!.start),
    );

    if (matchingCodeLenses.isEmpty) {
      var debugText = documentCodeLenses
          .map((codeLens) => '  ${codeLens.command?.title} (${codeLens.range})')
          .join('\n');
      fail(
        'Did not find "$codeLensTitle" ($sourceRange) CodeLens in $sourceUri.\n'
        'Found:\n$debugText',
      );
    } else if (matchingCodeLenses.length > 1) {
      var debugText = matchingCodeLenses
          .map((codeLens) => '  ${codeLens.command?.title} (${codeLens.range})')
          .join('\n');
      fail(
        'Found multiple "$codeLensTitle" ($sourceRange) CodeLens in $sourceUri.\n'
        'Found:\n$debugText',
      );
    }

    var codeLens = matchingCodeLenses.single;
    expect(codeLens.range, sourceRange);

    // Verify the command/args match what's documented in the our LSP readme.
    // Clients will need to provide this command and will implement it as
    // documented.
    expect(codeLens.command!.command, ClientCommands.goToLocation);
    expect(codeLens.command?.arguments, [
      Location(
        uri: targetUri,
        range: targetRange,
      ).toJson(), // Args are untyped, so toJson() to compare the Map.
    ]);
  }

  /// Expects no CodeLens in [uri]/[sourceUri] with the title [codeLensTitle].
  Future<void> expectNoCodeLenses([Uri? uri]) async {
    var documentCodeLenses = (await getCodeLens(uri ?? sourceUri)) ?? [];
    var matchingCodeLenses = documentCodeLenses.where(
      (codeLens) => codeLens.command?.title == codeLensTitle,
    );
    expect(matchingCodeLenses, isEmpty);
  }

  void setAugmentationContent(String content) {
    augmentationCode = TestCode.parse('''
part of 'main.dart';

$content
''');
    newFile(mainFileAugmentationPath, augmentationCode.code);
  }

  void setLibraryContent(String content) {
    libraryCode = TestCode.parse('''
part 'main_augmentation.dart';

$content
''');
    newFile(mainFilePath, libraryCode.code);
  }

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

    setClientSupportedCommands([ClientCommands.goToLocation]);
  }

  test_available_class() async {
    setLibraryContent(r'''
class [!A!] {}
''');
    setAugmentationContent(r'''
augment class [!A!] {}
''');

    await initialize();
    await expectNavigationCodeLens();
  }

  test_available_class_getter() async {
    setLibraryContent(r'''
class A {
  String get [!g!] => '';
}
''');
    setAugmentationContent(r'''
augment class A {
  augment String get [!g!];
}
''');

    await initialize();
    await expectNavigationCodeLens();
  }

  test_available_class_method() async {
    setLibraryContent(r'''
class A {
  void [!m!]() {}
}
''');
    setAugmentationContent(r'''
augment class A {
  augment void [!m!]();
}
''');

    await initialize();
    await expectNavigationCodeLens();
  }

  test_available_class_setter() async {
    setLibraryContent(r'''
class A {
  set [!g!](String value) {}
}
''');
    setAugmentationContent(r'''
augment class A {
  augment set [!g!](String value);
}
''');

    await initialize();
    await expectNavigationCodeLens();
  }

  test_available_enum() async {
    setLibraryContent(r'''
enum [!A!] {
  one,
}
''');
    setAugmentationContent(r'''
augment enum [!A!] {
  two,
}
''');

    await initialize();
    await expectNavigationCodeLens();
  }

  test_available_enum_member() async {
    setLibraryContent(r'''
enum A {
  [!one!],
}
''');
    setAugmentationContent(r'''
augment enum A {
  augment [!one!],
}
''');

    await initialize();
    await expectNavigationCodeLens();
  }

  test_available_topLevel_function() async {
    setLibraryContent(r'''
void [!f!]() {}
''');
    setAugmentationContent(r'''
augment void [!f!]();
''');

    await initialize();
    await expectNavigationCodeLens();
  }

  test_error_nonExistentFile() async {
    await initialize();

    await expectLater(
      expectNoCodeLenses(nonExistentFileUri),
      throwsA(
        isResponseError(
          ServerErrorCodes.invalidFilePath,
          message: 'File does not exist',
        ),
      ),
    );
  }

  test_unavailable_disabledEntirely() async {
    useSimpleClassAugmentation();

    await provideConfig(initialize, {'codeLens': false});
    await expectNoCodeLenses();
  }

  test_unavailable_disabledIndividually() async {
    useSimpleClassAugmentation();

    await provideConfig(initialize, {
      'codeLens': {codeLensSettingName: false},
    });
    await expectNoCodeLenses();
  }

  test_unavailable_noClientCommand() async {
    setClientSupportedCommands(null);

    useSimpleClassAugmentation();

    await initialize();
    await expectNoCodeLenses();
  }

  /// Sets up a simple class with augmentation that can be used by tests where
  /// the specifics are not important.
  void useSimpleClassAugmentation() {
    setLibraryContent(r'''
class A {}
''');
    setAugmentationContent(r'''
augment class A {}
''');
  }
}

/// Run all tests from [AbstractAugmentationCodeLensTest] looking for
/// "Go to Augmentation" that navigates from declarations in [mainFileUri] to
/// augmentations in [mainFileAugmentationUri].
@reflectiveTest
class AugmentationCodeLensTest extends AbstractAugmentationCodeLensTest {
  @override
  String get codeLensSettingName => 'augmentation';

  @override
  String get codeLensTitle => 'Go to Augmentation';

  @override
  Range get sourceRange => libraryCode.range.range;

  @override
  Uri get sourceUri => mainFileUri;

  @override
  Range get targetRange => augmentationCode.range.range;

  @override
  Uri get targetUri => mainFileAugmentationUri;

  test_available_class_augmentationOf_augmentationOf_declarationInAugmentationFile() async {
    setLibraryContent(r'');
    setAugmentationContent(r'''
class A {}
augment class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');

    await initialize();
    await expectNavigationCodeLens(
      sourceUri: mainFileAugmentationUri,
      sourceRange: augmentationCode.ranges[0].range,
      targetUri: mainFileAugmentationUri,
      targetRange: augmentationCode.ranges[1].range,
    );
  }

  test_available_class_augmentationOf_augmentationOf_declarationInLibraryFile() async {
    setLibraryContent(r'''
class A {}
''');
    setAugmentationContent(r'''
augment class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');

    await initialize();
    await expectNavigationCodeLens(
      sourceUri: mainFileAugmentationUri,
      sourceRange: augmentationCode.ranges[0].range,
      targetUri: mainFileAugmentationUri,
      targetRange: augmentationCode.ranges[1].range,
    );
  }

  test_available_class_augmentationOf_declarationInAugmentationFile() async {
    setLibraryContent('');
    setAugmentationContent(r'''
class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');

    await initialize();
    await expectNavigationCodeLens(
      sourceUri: mainFileAugmentationUri,
      sourceRange: augmentationCode.ranges[0].range,
      targetUri: mainFileAugmentationUri,
      targetRange: augmentationCode.ranges[1].range,
    );
  }
}

/// Run all tests from [AbstractAugmentationCodeLensTest] looking for
/// "Go to Augmented" that navigates from augmentations in
/// [mainFileAugmentationUri] to the original declarations in
/// [mainFileAugmentationUri].
@reflectiveTest
class AugmentedCodeLensTest extends AbstractAugmentationCodeLensTest {
  @override
  String get codeLensSettingName => 'augmented';

  @override
  String get codeLensTitle => 'Go to Augmented';

  @override
  Range get sourceRange => augmentationCode.range.range;

  @override
  Uri get sourceUri => mainFileAugmentationUri;

  @override
  Range get targetRange => libraryCode.range.range;

  @override
  Uri get targetUri => mainFileUri;

  test_available_class_augmentationOf_augmentationOf_declarationInAugmentationFile() async {
    setLibraryContent(r'');
    setAugmentationContent(r'''
class A {}
augment class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');

    await initialize();
    await expectNavigationCodeLens(
      sourceUri: mainFileAugmentationUri,
      sourceRange: augmentationCode.ranges[1].range,
      targetUri: mainFileAugmentationUri,
      targetRange: augmentationCode.ranges[0].range,
    );
  }

  test_available_class_augmentationOf_augmentationOf_declarationInLibraryFile() async {
    setLibraryContent(r'''
class A {}
''');
    setAugmentationContent(r'''
augment class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');

    await initialize();
    await expectNavigationCodeLens(
      sourceUri: mainFileAugmentationUri,
      sourceRange: augmentationCode.ranges[1].range,
      targetUri: mainFileAugmentationUri,
      targetRange: augmentationCode.ranges[0].range,
    );
  }

  test_available_class_augmentationOf_declarationInAugmentationFile() async {
    setLibraryContent('');
    setAugmentationContent(r'''
class /*[0*/A/*0]*/ {}
augment class /*[1*/A/*1]*/ {}
''');

    await initialize();
    await expectNavigationCodeLens(
      sourceUri: mainFileAugmentationUri,
      sourceRange: augmentationCode.ranges[1].range,
      targetUri: mainFileAugmentationUri,
      targetRange: augmentationCode.ranges[0].range,
    );
  }
}
