// 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 '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 '../tool/lsp_spec/matchers.dart';
import '../utils/test_code_extensions.dart';
import 'server_abstract.dart';

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

@reflectiveTest
class WorkspaceSymbolsTest extends AbstractLspAnalysisServerTest {
  Matcher isSymbol(
    String name, {
    String? containerName,
    SymbolKind? kind,
    Range? range,
  }) {
    return isA<SymbolInformation>()
        .having((s) => s.name, 'name', name)
        .having(
          (s) => s.containerName,
          'containerName',
          containerName ?? anything,
        )
        .having((s) => s.kind, 'kind', kind ?? anything)
        .having((s) => s.location.range, 'range', range ?? anything);
  }

  Future<void> test_cancellation() async {
    const content = '''
void f() {}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbolsRequest1 = makeRequest(
      Method.workspace_symbol,
      WorkspaceSymbolParams(query: 'f'),
    );
    var symbolsCancellation1 = makeNotification(
      Method.cancelRequest,
      CancelParams(id: symbolsRequest1.id),
    );
    var symbolsRequest2 = makeRequest(
      Method.workspace_symbol,
      WorkspaceSymbolParams(query: 'f'),
    );

    var responses = await Future.wait([
      sendRequestToServer(symbolsRequest1),
      sendNotificationToServer(symbolsCancellation1),
      sendRequestToServer(symbolsRequest2),
    ]);

    // Expect the first response was cancelled.
    var symbolsResponse1 = responses[0] as ResponseMessage;
    expect(symbolsResponse1.result, isNull);
    expect(symbolsResponse1.error, isNotNull);
    expect(
      symbolsResponse1.error,
      isResponseError(ErrorCodes.RequestCancelled),
    );

    // But second to complete normally.
    var symbolsResponse2 = responses[2] as ResponseMessage;
    expect(symbolsResponse2.result, hasLength(greaterThanOrEqualTo(1)));
    expect(symbolsResponse2.error, isNull);
  }

  Future<void> test_class_unnamed() async {
    const content = '''
class {
  void foo() {}
}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);

    failTestOnErrorDiagnostic = false;
    await provideConfig(initialize, {
      'includeDependenciesInWorkspaceSymbols': false,
    });

    var symbols = await getWorkspaceSymbols('foo');
    expect(symbols, isEmpty);
  }

  Future<void> test_constructor_primary_named_noBody() async {
    const content = '''
    /*[0*/class /*[1*/UniqueClassName.namedUnique(int a)/*1]*/;/*0]*/
    ''';

    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('UniqueClassName');
    expect(
      symbols,
      unorderedEquals([
        isSymbol(
          'UniqueClassName',
          kind: SymbolKind.Class,
          range: code.ranges[0].range,
        ),
        isSymbol(
          'UniqueClassName.namedUnique(…)',
          kind: SymbolKind.Constructor,
          range: code.ranges[1].range,
        ),
      ]),
    );
  }

  Future<void> test_constructor_primary_named_withBody() async {
    const content = '''
    /*[0*/class /*[1*/UniqueClassName.namedUnique(int a)/*1]*/ {
      this {}
    }/*0]*/
    ''';

    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('UniqueClassName');
    expect(
      symbols,
      unorderedEquals([
        isSymbol(
          'UniqueClassName',
          kind: SymbolKind.Class,
          range: code.ranges[0].range,
        ),
        isSymbol(
          'UniqueClassName.namedUnique(…)',
          kind: SymbolKind.Constructor,
          range: code.ranges[1].range,
        ),
      ]),
    );
  }

  Future<void> test_constructor_primary_noBody() async {
    const content = '''
    /*[0*/class /*[1*/UniqueClassName(int a)/*1]*/;/*0]*/
    ''';

    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('UniqueClassName');
    expect(
      symbols,
      unorderedEquals([
        isSymbol(
          'UniqueClassName',
          kind: SymbolKind.Class,
          range: code.ranges[0].range,
        ),
        isSymbol(
          'UniqueClassName(…)',
          kind: SymbolKind.Constructor,
          range: code.ranges[1].range,
        ),
      ]),
    );
  }

  Future<void> test_constructor_primary_withBody() async {
    const content = '''
    /*[0*/class /*[1*/UniqueClassName(int a)/*1]*/ {
      this {}
    }/*0]*/
    ''';

    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('UniqueClassName');
    expect(
      symbols,
      unorderedEquals([
        isSymbol(
          'UniqueClassName',
          kind: SymbolKind.Class,
          range: code.ranges[0].range,
        ),
        isSymbol(
          'UniqueClassName(…)',
          kind: SymbolKind.Constructor,
          range: code.ranges[1].range,
        ),
      ]),
    );
  }

  Future<void> test_constructors_doNotMatchNew() async {
    const content = '''
class UniqueClassName {
  UniqueClassName.new(int a);
}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('new');
    expect(
      symbols,
      isNot(
        anyOf([
          contains(isSymbol('UniqueClassName(…)')),
          // We never expect this format, but still ensure it never slips
          // through.
          contains(isSymbol('UniqueClassName.new(…)')),
        ]),
      ),
    );
  }

  Future<void> test_constructors_matchClassName() async {
    const content = '''
class UniqueClassName {
  UniqueClassName(int a);
  UniqueClassName.named(int a);
}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('UniqueClassName');
    expect(
      symbols,
      unorderedEquals([
        isSymbol('UniqueClassName', kind: SymbolKind.Class),
        isSymbol(
          'UniqueClassName(…)',
          kind: SymbolKind.Constructor,
          containerName: 'UniqueClassName',
        ),
        isSymbol(
          'UniqueClassName.named(…)',
          kind: SymbolKind.Constructor,
          containerName: 'UniqueClassName',
        ),
      ]),
    );
  }

  Future<void> test_constructors_matchConstructorName() async {
    const content = '''
class UniqueClassName {
  UniqueClassName.uniqueConstructorName(int a);
}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('uniqueConstructorName');
    expect(symbols, [
      isSymbol(
        'UniqueClassName.uniqueConstructorName(…)',
        kind: SymbolKind.Constructor,
        containerName: 'UniqueClassName',
      ),
    ]);
  }

  Future<void> test_dependencies_excluded() async {
    newFile(mainFilePath, 'class LocalClass12345 {}');
    await provideConfig(initialize, {
      'includeDependenciesInWorkspaceSymbols': false,
    });

    expect(await getWorkspaceSymbols('Duration'), isEmpty);
    expect(await getWorkspaceSymbols('LocalClass12345'), isNotEmpty);
  }

  Future<void> test_dependencies_included() async {
    newFile(mainFilePath, 'class LocalClass12345 {}');
    await provideConfig(initialize, {
      'includeDependenciesInWorkspaceSymbols': true,
    });

    expect(await getWorkspaceSymbols('Duration'), isNotEmpty);
    expect(await getWorkspaceSymbols('LocalClass12345'), isNotEmpty);
  }

  Future<void> test_dependencies_includedByDefault() async {
    newFile(mainFilePath, 'class LocalClass12345 {}');
    await initialize();

    expect(await getWorkspaceSymbols('Duration'), isNotEmpty);
    expect(await getWorkspaceSymbols('LocalClass12345'), isNotEmpty);
  }

  Future<void> test_enum_unnamed() async {
    const content = '''
enum {
  v
}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);

    failTestOnErrorDiagnostic = false;
    await provideConfig(initialize, {
      'includeDependenciesInWorkspaceSymbols': false,
    });

    var symbols = await getWorkspaceSymbols('v');
    expect(symbols, isEmpty);
  }

  Future<void> test_extensions() async {
    const content = '''
extension StringExtensions on String {}
extension on String {}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('S');

    var namedExtensions = symbols.firstWhere(
      (s) => s.name == 'StringExtensions',
    );
    expect(namedExtensions.kind, equals(SymbolKind.Class));
    expect(namedExtensions.containerName, isNull);

    // Unnamed extensions are not returned in Workspace Symbols.
  }

  Future<void> test_extensionType() async {
    const content = r'''
extension type MyExtensionType(int it) {}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('MyExt');

    var namedExtensions = symbols.firstWhere(
      (s) => s.name == 'MyExtensionType',
    );
    expect(namedExtensions.kind, equals(SymbolKind.Class));
    expect(namedExtensions.containerName, isNull);
  }

  Future<void> test_extensionType_method() async {
    const content = r'''
extension type E(int it) {
  void foo() {}
}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('foo');

    var namedExtensions = symbols.firstWhere(
      (s) => s.name == 'foo()' && s.containerName == 'E',
    );
    expect(namedExtensions.kind, equals(SymbolKind.Method));
    expect(namedExtensions.containerName, 'E');
  }

  Future<void> test_extensionType_unnamed() async {
    const content = '''
extension type (int it) {}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);

    failTestOnErrorDiagnostic = false;
    await provideConfig(initialize, {
      'includeDependenciesInWorkspaceSymbols': false,
    });

    var symbols = await getWorkspaceSymbols('it');
    expect(symbols, isEmpty);
  }

  Future<void> test_fullMatch() async {
    const content = '''
[!String topLevel = ''!];
class MyClass {
  int myField;
  MyClass(this.myField);
  myMethod() {}
}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('topLevel');

    var topLevel = symbols.firstWhere((s) => s.name == 'topLevel');
    expect(topLevel.kind, equals(SymbolKind.Variable));
    expect(topLevel.containerName, isNull);
    expect(topLevel.location.uri, equals(mainFileUri));
    expect(topLevel.location.range, equals(code.range.range));

    // Ensure we didn't get some things that definitely do not match.
    expect(symbols.any((s) => s.name.contains('MyClass')), isFalse);
    expect(symbols.any((s) => s.name.contains('myMethod')), isFalse);
  }

  Future<void> test_fuzzyMatch() async {
    const content = '''
String topLevel = '';
class MyClass {
  [!int myField!];
  MyClass(this.myField);
  myMethod() {}
}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    // meld should match myField
    var symbols = await getWorkspaceSymbols('meld');

    var field = symbols.firstWhere((s) => s.name == 'myField');
    expect(field.kind, equals(SymbolKind.Field));
    expect(field.containerName, equals('MyClass'));
    expect(field.location.uri, equals(mainFileUri));
    expect(field.location.range, equals(code.range.range));

    // Ensure we didn't get some things that definitely do not match.
    expect(symbols.any((s) => s.name.contains('MyClass')), isFalse);
    expect(symbols.any((s) => s.name.contains('myMethod')), isFalse);
  }

  Future<void> test_invalidParams() async {
    await initialize();

    // Create a request that doesn't supply the query param.
    var request = RequestMessage(
      id: Either2<int, String>.t1(1),
      method: Method.workspace_symbol,
      params: <String, dynamic>{},
      jsonrpc: jsonRpcVersion,
    );

    var response = await sendRequestToServer(request);
    var error = response.error!;
    expect(error.code, equals(ErrorCodes.InvalidParams));
    // Ensure the error is useful to the client.
    expect(
      error.message,
      equals(
        'Invalid params for workspace/symbol:\n'
        'params.query must not be undefined',
      ),
    );
  }

  Future<void> test_mixin_unnamed() async {
    const content = '''
mixin {
  void foo() {}
}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);

    failTestOnErrorDiagnostic = false;
    await provideConfig(initialize, {
      'includeDependenciesInWorkspaceSymbols': false,
    });

    var symbols = await getWorkspaceSymbols('foo');
    expect(symbols, isEmpty);
  }

  /// Ensure that multiple projects/drivers do not result in duplicate results
  /// for things referenced in both projects.
  Future<void> test_overlappingDrivers() async {
    // Reference an SDK lib.
    const content = "import 'dart:core';";
    // Project 1
    newFile(mainFilePath, content);
    // Project 2
    var otherFilePath = convertPath('/home/otherProject/foo.dart');
    newFile(otherFilePath, content);

    // Initialize with both projects as roots.
    await initialize(
      workspaceFolders: [
        projectFolderUri,
        pathContext.toUri(convertPath('/home/otherProject')),
      ],
    );

    // Search for something in the SDK that's referenced by both projects and
    // expect it only shows up once.
    var symbols = await getWorkspaceSymbols('Duration');
    expect(symbols.where((s) => s.name == 'Duration'), hasLength(1));
  }

  Future<void> test_partialMatch() async {
    const content = '''
String topLevel = '';
class MyClass {
  /*[0*/int myField/*0]*/;
  MyClass(this.myField);
  /*[1*/myMethod() {}/*1]*/
  /*[2*/myMethodWithArgs(int a) {}/*2]*/
}
''';
    var code = TestCode.parse(content);
    newFile(mainFilePath, code.code);
    await initialize();

    var symbols = await getWorkspaceSymbols('my');
    var ranges = code.ranges.ranges;
    var fieldRange = ranges[0];
    var methodRange = ranges[1];
    var methodWithArgsRange = ranges[2];

    var field = symbols.firstWhere((s) => s.name == 'myField');
    expect(field.kind, equals(SymbolKind.Field));
    expect(field.containerName, equals('MyClass'));
    expect(field.location.uri, equals(mainFileUri));
    expect(field.location.range, equals(fieldRange));

    var klass = symbols.firstWhere((s) => s.name == 'MyClass');
    expect(klass.kind, equals(SymbolKind.Class));
    expect(klass.containerName, isNull);
    expect(klass.location.uri, equals(mainFileUri));

    var method = symbols.firstWhere((s) => s.name == 'myMethod()');
    expect(method.kind, equals(SymbolKind.Method));
    expect(method.containerName, equals('MyClass'));
    expect(method.location.uri, equals(mainFileUri));
    expect(method.location.range, equals(methodRange));

    var methodWithArgs = symbols.firstWhere(
      (s) => s.name == 'myMethodWithArgs(…)',
    );
    expect(methodWithArgs.kind, equals(SymbolKind.Method));
    expect(methodWithArgs.containerName, equals('MyClass'));
    expect(methodWithArgs.location.uri, equals(mainFileUri));
    expect(methodWithArgs.location.range, equals(methodWithArgsRange));

    // Ensure we didn't get some things that definitely do not match.
    expect(symbols.any((s) => s.name == 'topLevel'), isFalse);
  }
}
