// 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 'package:analysis_server/lsp_protocol/protocol.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 '../utils/test_code_extensions.dart';
import 'server_abstract.dart';

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

mixin SignatureHelpMixin on AbstractLspAnalysisServerTest {
  Future<void> expectNoSignature(
    String content, {
    Uri? fileUri,
    Position? position,
    SignatureHelpContext? context,
  }) async {
    var code = TestCode.parse(content);
    fileUri ??= mainFileUri;
    position ??= code.position.position;

    await initialize();
    await openFile(fileUri, code.code);

    var res = await getSignatureHelp(fileUri, position, context);
    expect(res, isNull);
  }

  Future<void> _expectSignature(
    String content,
    String expectedLabel, {
    String? expectedDoc,
    List<ParameterInformation>? expectedParams,
    MarkupKind? expectedFormat = MarkupKind.Markdown,
    SignatureHelpContext? context,
    _FileState state = _FileState.open,
    int? activeParameter,
  }) async {
    var code = TestCode.parse(content);
    if (state == _FileState.closed) {
      newFile(mainFilePath, code.code);
    }
    await initialize();
    if (state == _FileState.open) {
      await openFile(mainFileUri, code.code);
    }
    await initialAnalysis;

    var res = (await getSignatureHelp(
      mainFileUri,
      code.position.position,
      context,
    ))!;

    if (activeParameter != null) {
      expect(res.activeParameter, activeParameter);
    }
    expect(res.activeSignature, equals(0));
    expect(res.signatures, hasLength(1));
    var sig = res.signatures.first;
    expect(sig.label, equals(expectedLabel));
    if (expectedParams != null) {
      expect(sig.parameters, equals(expectedParams));
    }

    if (expectedDoc != null) {
      // Test the format matches the tests expectation.
      // For clients that don't support MarkupContent it'll be a plain string,
      // but otherwise it'll be a MarkupContent of type PlainText or Markdown.
      var doc = sig.documentation!;
      if (expectedFormat == null) {
        // Plain string.
        expect(doc.valueEquals(expectedDoc), isTrue);
      } else {
        var expected = MarkupContent(kind: expectedFormat, value: expectedDoc);
        expect(doc.valueEquals(expected), isTrue);
      }
    }
  }
}

@reflectiveTest
class SignatureHelpTest extends AbstractLspAnalysisServerTest
    with SignatureHelpMixin {
  Future<void> assertArgsDocumentation(
    String? preference, {
    required bool includesSummary,
    required bool includesFull,
  }) {
    var content = '''
class A {
  /// Summary.
  ///
  /// Full.
  A();
}

final a = A(^);
''';

    return assertDocumentation(
      preference,
      content,
      includesSummary: includesSummary,
      includesFull: includesFull,
    );
  }

  /// Checks whether the correct types of documentation are returned in
  /// signature help for function arguments based on [preference].
  Future<void> assertDocumentation(
    String? preference,
    String content, {
    required bool includesSummary,
    required bool includesFull,
  }) async {
    var code = TestCode.parse(content);
    await provideConfig(initialize, {'documentation': ?preference});
    await openFile(mainFileUri, code.code);
    await initialAnalysis;
    var signatureHelp = await getSignatureHelp(
      mainFileUri,
      code.position.position,
    );
    var docs = signatureHelp!.signatures.single.documentation?.map(
      (markup) => markup.value,
      (string) => string,
    );

    if (includesSummary) {
      expect(docs, contains('Summary.'));
    } else {
      expect(docs, isNot(contains('Summary.')));
    }

    if (includesFull) {
      expect(docs, contains('Full.'));
    } else {
      expect(docs, isNot(contains('Full.')));
    }
  }

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

    // Signature help tests have incomplete signatures.
    failTestOnErrorDiagnostic = false;

    setSignatureHelpContentFormat([MarkupKind.Markdown]);
  }

  Future<void> test_activeParameter_named_withLabel_inOrder() async {
    var content = '''
void f({int a, int b}) {
  f(a: 1, b: ^);
}
''';

    await _expectSignature(
      content,
      'f({int a, int b})',
      // We matched on the name.
      activeParameter: 1,
    );
  }

  Future<void> test_activeParameter_named_withLabel_invalid() async {
    var content = '''
void f({int a}) {
  f(b: ^);
}
''';

    await _expectSignature(
      content,
      'f({int a})',
      // First invalid index, because we don't match a named parameter.
      activeParameter: 1,
    );
  }

  Future<void> test_activeParameter_named_withLabel_outOfOrder1() async {
    var content = '''
void f({int a, int b}) {
  f(b: ^);
}
''';

    await _expectSignature(
      content,
      'f({int a, int b})',
      // We matched on the name.
      activeParameter: 1,
    );
  }

  Future<void> test_activeParameter_named_withLabel_outOfOrder2() async {
    var content = '''
void f({int a, int b, int c}) {
  f(a: 1, c: ^);
}
''';

    await _expectSignature(
      content,
      'f({int a, int b, int c})',
      // We matched on the name.
      activeParameter: 2,
    );
  }

  Future<void> test_activeParameter_named_withoutLabel1() async {
    var content = '''
void f({int a}) {
  f(^);
}
''';

    await _expectSignature(
      content,
      'f({int a})',
      // First invalid index, because we don't try to guess named parameters.
      activeParameter: 1,
    );
  }

  Future<void> test_activeParameter_named_withoutLabel2() async {
    var content = '''
void f({int a, int b}) {
  f(a: 1, ^);
}
''';

    await _expectSignature(
      content,
      'f({int a, int b})',
      // First invalid index, because we don't try to guess named parameters.
      activeParameter: 2,
    );
  }

  Future<void> test_activeParameter_none() async {
    var content = '''
void f() {
  f(^);
}
''';

    await _expectSignature(
      content,
      'f()',
      // First invalid index (there is no 0th parameter).
      activeParameter: 0,
    );
  }

  Future<void> test_activeParameter_positional1() async {
    var content = '''
void f(int a) {
  f(^);
}
''';

    await _expectSignature(
      content,
      'f(int a)',
      // Matched on index.
      activeParameter: 0,
    );
  }

  Future<void> test_activeParameter_positional2() async {
    var content = '''
void f(int a, int b) {
  f(1, ^);
}
''';

    await _expectSignature(
      content,
      'f(int a, int b)',
      // Matched on index.
      activeParameter: 1,
    );
  }

  Future<void> test_activeParameter_positional_afterNamed() async {
    var content = '''
void f(int a, int b, int c, {int d}) {
  f(1, d: 2, 3, ^);
}
''';

    await _expectSignature(
      content,
      'f(int a, int b, int c, {int d})',
      // Matched on index of positionals (c).
      activeParameter: 2,
    );
  }

  Future<void> test_activeParameter_positional_withExprsesion() async {
    var content = '''
void f(int a) {
  f(1 + ^);
}
''';

    await _expectSignature(
      content,
      'f(int a)',
      // Matched on index.
      activeParameter: 0,
    );
  }

  Future<void> test_activeParameter_positionalinvalid() async {
    var content = '''
void f(int a, int b) {
  f(1, 2, ^);
}
''';

    await _expectSignature(
      content,
      'f(int a, int b)',
      // First invalid index because there's no valid parameter.
      activeParameter: 2,
    );
  }

  Future<void> test_augmentation_method() async {
    var content = '''
part 'a.dart';

class Foo {}

void bar() {
  Foo().myMethod(^);
}
''';

    var augmentationFilePath = join(projectFolderPath, 'lib', 'a.dart');
    var augmentationCode = '''
part of 'main.dart';

augment class Foo {
  /// My method.
  void myMethod(String s) {}
}
''';
    newFile(augmentationFilePath, augmentationCode);
    var expectedLabel = 'myMethod(String s)';
    var expectedDoc = 'My method.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [ParameterInformation(label: 'String s')],
    );
  }

  Future<void> test_callableClass() async {
    var content = '''
class Foo {
  /// Does foo.
  int call(String s, int i) {
    var foo = Foo();
    return foo(^);
  }
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
    );
  }

  Future<void> test_dartDocMacro() async {
    setSignatureHelpContentFormat(null);

    var content = '''
/// {@template template_name}
/// This is shared content.
/// {@endtemplate}
const String bar = null;

/// {@macro template_name}
foo(String s, int i) {
  foo(^);
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'This is shared content.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
      expectedFormat: null,
    );
  }

  Future<void> test_dartDocPreference_full() => assertArgsDocumentation(
    'full',
    includesSummary: true,
    includesFull: true,
  );

  Future<void> test_dartDocPreference_summary() => assertArgsDocumentation(
    'summary',
    includesSummary: true,
    // Doc preferences don't apply to single-result requests so always expect
    // full docs.
    includesFull: true,
  );

  /// No preference should result in full docs.
  Future<void> test_dartDocPreference_unset() =>
      assertArgsDocumentation(null, includesSummary: true, includesFull: true);

  Future<void> test_error_methodInvocation_importPrefix() async {
    var content = '''
import 'dart:async' as prefix;

void f() {
  prefix(^);
}
''';

    // Expect no result.
    await expectNoSignature(content);
  }

  Future<void> test_extensionType() async {
    var content = '''
class A {
  void f(int a) {}
}

extension type E(A a) {
  void f(int e) {}
}

void f() {
  final e = E(A());
  e.f(^);
}
''';

    await _expectSignature(
      content,
      'f(int e)',
      expectedParams: [ParameterInformation(label: 'int e')],
    );
  }

  Future<void> test_formats_markdown() async {
    var content = '''
/// Does foo.
foo(String s, int i) {
  foo(^);
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
    );
  }

  Future<void> test_formats_notSupported() async {
    setSignatureHelpContentFormat(null);

    var content = '''
/// Does foo.
foo(String s, int i) {
  foo(^);
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
      expectedFormat: null,
    );
  }

  Future<void> test_formats_plainTextOnly() async {
    setSignatureHelpContentFormat([MarkupKind.PlainText]);

    var content = '''
/// Does foo.
foo(String s, int i) {
  foo(^);
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
      expectedFormat: MarkupKind.PlainText,
    );
  }

  Future<void> test_formats_plainTextPreferred() async {
    setSignatureHelpContentFormat([MarkupKind.PlainText, MarkupKind.Markdown]);

    var content = '''
/// Does foo.
foo(String s, int i) {
  foo(^);
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'Does foo.';

    // We say we prefer PlainText as a client, but since we only really
    // support Markdown and the client supports it, we expect the server
    // to provide Markdown.

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
    );
  }

  Future<void> test_functionExpression_local() async {
    var content = '''

void f() {
  var foo = (String s, int i) => s;
  foo(^);
}
''';
    var expectedLabel = 'foo(String s, int i)';

    await _expectSignature(
      content,
      expectedLabel,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
    );
  }

  Future<void> test_functionExpression_topLevel() async {
    var content = '''
/// Does foo.
var foo = (String s, int i) => s;

void f() {
  foo(^);
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
    );
  }

  Future<void> test_manualTrigger_invalidLocation() async {
    // If the user invokes signature help, we should show it even if it's a
    // location where we wouldn't automatically trigger (for example in a string).
    var content = '''
/// Does foo.
foo(String s, int i) {
  foo('this is a (^test');
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
      context: SignatureHelpContext(
        triggerKind: SignatureHelpTriggerKind.Invoked,
        isRetrigger: false,
      ),
    );
  }

  Future<void> test_noDefaultConstructor() async {
    var content = '''
class A {
  A._();
}

final a = A(^);
''';

    await expectNoSignature(content);
  }

  Future<void> test_nonDartFile() async {
    await expectNoSignature(
      simplePubspecContent,
      fileUri: pubspecFileUri,
      position: startOfDocPos,
    );
  }

  /// Keywords for declaraing parameters should not be shown because they don't
  /// change the parameters and are just visual noise.
  Future<void> test_params_final() async {
    var content = '''
foo(final String s) {
  foo(^);
}
''';

    var expectedLabel = 'foo(String s)';

    await _expectSignature(
      content,
      expectedLabel,
      expectedParams: [ParameterInformation(label: 'String s')],
    );
  }

  Future<void> test_params_multipleNamed() async {
    var content = '''
/// Does foo.
foo(String s, {bool b = true, bool a}) {
  foo(^);
}
''';

    var expectedLabel = 'foo(String s, {bool b = true, bool a})';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'bool b = true'),
        ParameterInformation(label: 'bool a'),
      ],
    );
  }

  Future<void> test_params_multipleNamed_retrigger() async {
    var content = '''
/// Does foo.
foo(String s, {bool b = true, bool a}) {
  foo('s',^);
}
''';

    var expectedLabel = 'foo(String s, {bool b = true, bool a})';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'bool b = true'),
        ParameterInformation(label: 'bool a'),
      ],
    );
  }

  Future<void> test_params_multipleOptional() async {
    var content = '''
/// Does foo.
foo(String s, [bool b = true, bool a]) {
  foo(^);
}
''';

    var expectedLabel = 'foo(String s, [bool b = true, bool a])';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'bool b = true'),
        ParameterInformation(label: 'bool a'),
      ],
    );
  }

  Future<void> test_params_named() async {
    var content = '''
/// Does foo.
foo(String s, {bool b = true}) {
  foo(^);
}
''';

    var expectedLabel = 'foo(String s, {bool b = true})';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'bool b = true'),
      ],
    );
  }

  Future<void> test_params_optional() async {
    var content = '''
/// Does foo.
foo(String s, [bool b = true]) {
  foo(^);
}
''';

    var expectedLabel = 'foo(String s, [bool b = true])';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'bool b = true'),
      ],
    );
  }

  Future<void> test_params_primaryConstructor() async {
    var content = '''
class A(final int x, var int y, int z);

final a = A(^);
''';
    var expectedLabel = 'A(int x, int y, int z)';
    var expectedParams = [
      ParameterInformation(label: 'int x'),
      ParameterInformation(label: 'int y'),
      ParameterInformation(label: 'int z'),
    ];

    await _expectSignature(
      content,
      expectedLabel,
      expectedParams: expectedParams,
    );
  }

  Future<void> test_params_recordType() async {
    var content = '''
/// Does something.
void f((String, int) r) {
  f(^);
}
''';

    var expectedLabel = 'f((String, int) r)';
    var expectedDoc = 'Does something.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [ParameterInformation(label: '(String, int) r')],
    );
  }

  Future<void> test_params_requiredNamed() async {
    var content = '''
/// Does foo.
foo(String s, {bool? b = true, required bool a}) {
  foo(^);
}
''';

    var expectedLabel = 'foo(String s, {bool? b = true, required bool a})';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'bool? b = true'),
        ParameterInformation(label: 'required bool a'),
      ],
    );
  }

  Future<void> test_simple() async {
    var content = '''
/// Does foo.
foo(String s, int i) {
  foo(^);
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
    );
  }

  Future<void> test_triggerCharacter_invalidLocation() async {
    // The client will automatically trigger when the user types ( so we need to
    // ignore it when we're not in a suitable location.
    var content = '''
/// Does foo.
foo(String s, int i) {
  foo('this is a (^test');
}
''';

    // Expect no result.
    await expectNoSignature(
      content,
      context: SignatureHelpContext(
        triggerKind: SignatureHelpTriggerKind.TriggerCharacter,
        isRetrigger: false,
      ),
    );
  }

  Future<void> test_triggerCharacter_validLocation() async {
    var content = '''
/// Does foo.
foo(String s, int i) {
  foo(^
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
      context: SignatureHelpContext(
        triggerKind: SignatureHelpTriggerKind.Invoked,
        isRetrigger: false,
      ),
    );
  }

  Future<void> test_typeArgs_dartDocPreference_full() =>
      assertArgsDocumentation(
        'full',
        includesSummary: true,
        includesFull: true,
      );

  Future<void>
  test_typeArgs_dartDocPreference_summary() => assertArgsDocumentation(
    'summary',
    includesSummary: true,
    // Doc preferences don't apply to single-result requests so always expect
    // full docs.
    includesFull: true,
  );

  /// No preference should result in full docs.
  Future<void> test_typeArgs_dartDocPreference_unset() =>
      assertArgsDocumentation(null, includesSummary: true, includesFull: true);

  Future<void> test_typeParams_class() async {
    var content = '''
/// My Foo.
class Foo<T1, T2 extends String> {}

class Bar extends Foo<^> {}
''';

    const expectedLabel = 'class Foo<T1, T2 extends String>';
    const expectedDoc = 'My Foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'T1'),
        ParameterInformation(label: 'T2 extends String'),
      ],
    );
  }

  Future<void> test_typeParams_function() async {
    var content = '''
/// My Foo.
void foo<T1, T2 extends String>() {
  foo<^>();
}
''';

    const expectedLabel = 'void foo<T1, T2 extends String>()';
    const expectedDoc = 'My Foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'T1'),
        ParameterInformation(label: 'T2 extends String'),
      ],
    );
  }

  Future<void> test_typeParams_method() async {
    var content = '''
class Foo {
  /// My Foo.
  void foo<T1, T2 extends String>() {
    foo<^>();
  }
}
''';

    const expectedLabel = 'void foo<T1, T2 extends String>()';
    const expectedDoc = 'My Foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'T1'),
        ParameterInformation(label: 'T2 extends String'),
      ],
    );
  }

  Future<void> test_typeParams_primaryConstructor() async {
    var content = '''
class A<T1, T2>(int x);
var a = A<^>();
''';

    var expectedLabel = 'class A<T1, T2>';
    var expectedParams = [
      ParameterInformation(label: 'T1'),
      ParameterInformation(label: 'T2'),
    ];

    await _expectSignature(
      content,
      expectedLabel,
      expectedParams: expectedParams,
    );
  }

  Future<void> test_unopenFile() async {
    var content = '''
/// Does foo.
foo(String s, int i) {
  foo(^);
}
''';
    var expectedLabel = 'foo(String s, int i)';
    var expectedDoc = 'Does foo.';

    await _expectSignature(
      content,
      expectedLabel,
      expectedDoc: expectedDoc,
      expectedParams: [
        ParameterInformation(label: 'String s'),
        ParameterInformation(label: 'int i'),
      ],
      state: _FileState.closed,
    );
  }
}

enum _FileState { open, closed }
