// Copyright (c) 2016, 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/services/search/search_engine.dart';
import 'package:analysis_server/src/services/search/search_engine_internal.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/test_utilities/find_element2.dart';
import 'package:analyzer/src/test_utilities/find_node.dart';
import 'package:analyzer/src/test_utilities/test_code_format.dart';
import 'package:analyzer/src/util/performance/operation_performance.dart';
import 'package:analyzer_testing/package_config_file_builder.dart';
import 'package:collection/collection.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../../abstract_context.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(SearchEngineImplTest);
    defineReflectiveTests(SearchEngineImplWithNonFunctionTypeAliasesTest);
  });
}

// TODO(scheglov): This class does not really belong here.
// Consider merging it into [AbstractContextTest].
class PubPackageResolutionTest extends AbstractContextTest {
  late ResolvedUnitResult result;
  late FindNode findNode;
  late FindElement2 findElement2;

  void addTestFile(String content) {
    newFile(testFile.path, content);
  }

  /// Resolve the [file] into [result].
  Future<void> resolveFile2(File file) async {
    result = await getResolvedUnit(file);

    findNode = FindNode(result.content, result.unit);
    findElement2 = FindElement2(result.unit);
  }

  /// Put the [code] into the test file, and resolve it.
  Future<void> resolveTestCode(String code) {
    addTestFile(code);
    return resolveTestFile();
  }

  Future<void> resolveTestFile() {
    return resolveFile2(testFile);
  }
}

@reflectiveTest
class SearchEngineImplTest extends PubPackageResolutionTest {
  SearchEngineImpl get searchEngine {
    return SearchEngineImpl(allDrivers);
  }

  Future<TestCode> resolveParsedCode(String content) async {
    var code = TestCode.parseNormalized(content);
    await resolveTestCode(code.code);
    return code;
  }

  Future<void> test_membersOfSubtypes_classByClass_hasMembers() async {
    var a = newFile('$testPackageLibPath/a.dart', '''
class A {
  void a() {}
  void b() {}
  void c() {}
}
''');

    newFile('$testPackageLibPath/b.dart', '''
import 'a.dart';
class B extends A {
  void a() {}
}
''');

    newFile('$testPackageLibPath/c.dart', '''
import 'a.dart';
class C extends A {
  void b() {}
}
''');

    await resolveFile2(a);
    var A = findElement2.class_('A');

    var members = await searchEngine.membersOfSubtypes(A);
    expect(members, unorderedEquals(['a', 'b']));
  }

  Future<void> test_membersOfSubtypes_enum_implements_hasMembers() async {
    await resolveTestCode('''
class A {
  void foo() {}
}

enum E implements A {
  v;
  void foo() {}
}
''');

    var A = findElement2.class_('A');
    var members = await searchEngine.membersOfSubtypes(A);
    expect(members, unorderedEquals(['foo']));
  }

  Future<void> test_membersOfSubtypes_enum_with_hasMembers() async {
    await resolveTestCode('''
mixin M {
  void foo() {}
}

enum E with M {
  v;
  void foo() {}
}
''');

    var M = findElement2.mixin('M');
    var members = await searchEngine.membersOfSubtypes(M);
    expect(members, unorderedEquals(['foo']));
  }

  Future<void> test_membersOfSubtypes_noMembers() async {
    var a = newFile('$testPackageLibPath/a.dart', '''
class A {
  void a() {}
  void b() {}
  void c() {}
}
''');

    newFile('$testPackageLibPath/b.dart', '''
import 'a.dart';
class B extends A {}
''');

    await resolveFile2(a);
    var A = findElement2.class_('A');

    var members = await searchEngine.membersOfSubtypes(A);
    expect(members, isEmpty);
  }

  Future<void> test_membersOfSubtypes_noSubtypes() async {
    var a = newFile('$testPackageLibPath/a.dart', '''
class A {
  void a() {}
  void b() {}
  void c() {}
}
''');

    newFile('$testPackageLibPath/b.dart', '''
import 'a.dart';
class B {
  void a() {}
}
''');

    await resolveFile2(a);
    var A = findElement2.class_('A');

    var members = await searchEngine.membersOfSubtypes(A);
    expect(members, isNull);
  }

  Future<void> test_membersOfSubtypes_private() async {
    var a = newFile('$testPackageLibPath/a.dart', '''
class A {
  void a() {}
  void _b() {}
  void _c() {}
}
class B extends A {
  void _b() {}
}
''');

    newFile('$testPackageLibPath/b.dart', '''
import 'a.dart';
class C extends A {
  void a() {}
  void _c() {}
}
class D extends B {
  void _c() {}
}
''');

    await resolveFile2(a);
    var A = findElement2.class_('A');

    var members = await searchEngine.membersOfSubtypes(A);
    expect(members, unorderedEquals(['a', '_b']));
  }

  Future<void> test_searchAllSubtypes() async {
    await resolveTestCode('''
class T {}
class A extends T {}
class B extends A {}
class C implements B {}
''');

    var element = findElement2.class_('T');

    var subtypes = <InterfaceElement>{};
    await searchEngine.appendAllSubtypes(
      element,
      subtypes,
      OperationPerformanceImpl('<root>'),
    );
    expect(subtypes, hasLength(3));
    _assertContainsClass(subtypes, 'A');
    _assertContainsClass(subtypes, 'B');
    _assertContainsClass(subtypes, 'C');
  }

  Future<void> test_searchAllSubtypes_acrossDrivers() async {
    var aaaRootPath = _configureForPackage_aaa();

    var a = newFile('$aaaRootPath/lib/a.dart', '''
class T {}
class A extends T {}
''');

    newFile('$testPackageLibPath/b.dart', '''
import 'package:aaa/a.dart';
class B extends A {}
class C extends B {}
''');

    await resolveFile2(a);
    var element = findElement2.class_('T');

    var subtypes = <InterfaceElement>{};
    await searchEngine.appendAllSubtypes(
      element,
      subtypes,
      OperationPerformanceImpl('<root>'),
    );
    expect(subtypes, hasLength(3));
    _assertContainsClass(subtypes, 'A');
    _assertContainsClass(subtypes, 'B');
    _assertContainsClass(subtypes, 'C');
  }

  Future<void> test_searchAllSubtypes_extensionType() async {
    await resolveTestCode('''
class A {}
extension type B(int it) implements A {}
extension type C(int it) implements A {}
''');

    var element = findElement2.class_('A');

    var subtypes = <InterfaceElement>{};
    await searchEngine.appendAllSubtypes(
      element,
      subtypes,
      OperationPerformanceImpl('<root>'),
    );
    expect(subtypes, hasLength(2));
    _assertContainsClass(subtypes, 'B');
    _assertContainsClass(subtypes, 'C');
  }

  Future<void> test_searchAllSubtypes_mixin() async {
    await resolveTestCode('''
class T {}

mixin A on T {}
mixin B implements T {}

class C extends T {}

mixin D on C {}
mixin E implements C {}
''');

    var element = findElement2.class_('T');

    var subtypes = <InterfaceElement>{};
    await searchEngine.appendAllSubtypes(
      element,
      subtypes,
      OperationPerformanceImpl('<root>'),
    );
    expect(subtypes, hasLength(5));
    _assertContainsClass(subtypes, 'A');
    _assertContainsClass(subtypes, 'B');
    _assertContainsClass(subtypes, 'C');
    _assertContainsClass(subtypes, 'D');
    _assertContainsClass(subtypes, 'E');
  }

  Future<void> test_searchMemberDeclarations() async {
    var codeA = TestCode.parseNormalized('''
class A {
  int ^test;
  int testTwo;
}
''');
    var codeB = TestCode.parseNormalized('''
class B {
  void ^test() {}
  void testTwo() {}
}
int test;
''');

    newFile('$testPackageLibPath/a.dart', codeA.code);
    newFile('$testPackageLibPath/b.dart', codeB.code);

    var matches = await searchEngine.searchMemberDeclarations('test');
    expect(matches, hasLength(2));

    void assertHasElement(String name, int nameOffset) {
      expect(
        matches,
        contains(
          predicate(
            (SearchMatch m) =>
                m.kind == MatchKind.DECLARATION &&
                m.element.name == name &&
                m.element.firstFragment.nameOffset == nameOffset,
          ),
        ),
      );
    }

    assertHasElement('test', codeA.position.offset);
    assertHasElement('test', codeB.position.offset);
  }

  Future<void> test_searchMemberReferences() async {
    newFile('$testPackageLibPath/a.dart', '''
class A {
  int test;
}
foo(p) {
  p.test;
}
''');

    newFile('$testPackageLibPath/b.dart', '''
import 'a.dart';
bar(p) {
  p.test = 1;
}
''');

    var matches = await searchEngine.searchMemberReferences('test');
    expect(matches, hasLength(2));
    expect(
      matches,
      contains(
        predicate(
          (SearchMatch m) =>
              m.element.name == 'foo' || m.kind == MatchKind.READ,
        ),
      ),
    );
    expect(
      matches,
      contains(
        predicate(
          (SearchMatch m) =>
              m.element.name == 'bar' || m.kind == MatchKind.WRITE,
        ),
      ),
    );
  }

  Future<void> test_searchReferences() async {
    var aaaRootPath = _configureForPackage_aaa();

    newFile('$aaaRootPath/lib/a.dart', '''
class T {}
T a;
''');

    await resolveTestCode('''
import 'package:aaa/a.dart';
T b;
''');

    var element = findElement2.importFind('package:aaa/a.dart').class_('T');
    var matches = await searchEngine.searchReferences(element);
    expect(matches, hasLength(2));
    expect(
      matches,
      contains(predicate((SearchMatch m) => m.element.name == 'a')),
    );
    expect(
      matches,
      contains(predicate((SearchMatch m) => m.element.name == 'b')),
    );
  }

  Future<void> test_searchReferences_discover_owned() async {
    var aaaRootPath = _configureForPackage_aaa();

    var a = newFile('$aaaRootPath/lib/a.dart', '''
int a;
''').path;

    var t = newFile('$testPackageLibPath/lib/t.dart', '''
import 'package:aaa/a.dart';
int t;
''').path;

    var coreLibResult =
        await driverFor(testFile).getLibraryByUri('dart:core')
            as LibraryElementResult;
    var intElement = coreLibResult.element.classes.firstWhereOrNull(
      (e) => e.name == 'int',
    )!;

    var matches = await searchEngine.searchReferences(intElement);

    void assertHasOne(String path, String name) {
      expect(
        matches.where((m) {
          var element = m.element;
          return element.name == name &&
              element.library?.firstFragment.source.fullName == path;
        }),
        hasLength(1),
      );
    }

    assertHasOne(t, 't');
    assertHasOne(a, 'a');
  }

  Future<void> test_searchReferences_enum_constructor_named() async {
    var code = await resolveParsedCode('''
enum E {
  v^.named();
  const E.named();
}
''');

    var element = findElement2.constructor('named');
    var matches = await searchEngine.searchReferences(element);
    expect(
      matches,
      unorderedEquals([
        predicate((SearchMatch m) {
          return m.kind == MatchKind.INVOCATION &&
              identical(m.element, findElement2.field('v')) &&
              m.sourceRange.offset == code.position.offset &&
              m.sourceRange.length == '.named'.length;
        }),
      ]),
    );
  }

  Future<void> test_searchReferences_enum_constructor_unnamed() async {
    var code = await resolveParsedCode('''
enum E {
  v1/*0*/,
  v2/*1*/(),
  v3/*2*/.new(),
}
''');

    var element = findElement2.unnamedConstructor('E');
    var matches = await searchEngine.searchReferences(element);
    expect(
      matches,
      unorderedEquals([
        predicate((SearchMatch m) {
          return m.kind ==
                  MatchKind.INVOCATION_BY_ENUM_CONSTANT_WITHOUT_ARGUMENTS &&
              identical(m.element, findElement2.field('v1')) &&
              m.sourceRange.offset == code.positions[0].offset &&
              m.sourceRange.length == 0;
        }),
        predicate((SearchMatch m) {
          return m.kind == MatchKind.INVOCATION &&
              identical(m.element, findElement2.field('v2')) &&
              m.sourceRange.offset == code.positions[1].offset &&
              m.sourceRange.length == 0;
        }),
        predicate((SearchMatch m) {
          return m.kind == MatchKind.INVOCATION &&
              identical(m.element, findElement2.field('v3')) &&
              m.sourceRange.offset == code.positions[2].offset &&
              m.sourceRange.length == '.new'.length;
        }),
      ]),
    );
  }

  Future<void> test_searchReferences_extensionType() async {
    var code = await resolveParsedCode('''
extension type A(int it) {}

void f(^A a) {}
''');

    var element = findElement2.extensionType('A');
    var matches = await searchEngine.searchReferences(element);
    expect(
      matches,
      unorderedEquals([
        predicate((SearchMatch m) {
          return m.kind == MatchKind.REFERENCE &&
              identical(m.element, findElement2.parameter('a')) &&
              m.sourceRange.offset == code.position.offset &&
              m.sourceRange.length == 'A'.length;
        }),
      ]),
    );
  }

  Future<void>
  test_searchReferences_parameter_ofConstructor_super_named() async {
    var code = await resolveParsedCode('''
class A {
  A({required int a});
}
class B extends A {
  B({required super.^a});
}
''');

    var element = findElement2.unnamedConstructor('A').parameter('a');
    var matches = await searchEngine.searchReferences(element);
    expect(
      matches,
      unorderedEquals([
        predicate((SearchMatch m) {
          return m.kind == MatchKind.REFERENCE_BY_NAMED_ARGUMENT &&
              identical(
                m.element,
                findElement2.unnamedConstructor('B').superFormalParameter('a'),
              ) &&
              m.sourceRange.offset == code.position.offset &&
              m.sourceRange.length == 1;
        }),
      ]),
    );
  }

  Future<void> test_searchReferences_parameter_topLevelShadow_wildcard() async {
    var code = await resolveParsedCode('''
int _ = 0;
int f(int _) => ^_;
''');

    var parameter = findElement2.parameter('_');
    var parameterMatches = await searchEngine.searchReferences(parameter);
    expect(parameterMatches, isEmpty);

    var topLevelVariable = findElement2.topVar('_');
    var topLevelVariableMatches = await searchEngine.searchReferences(
      topLevelVariable,
    );
    expect(
      topLevelVariableMatches,
      unorderedEquals([
        predicate((SearchMatch m) {
          return m.kind == MatchKind.READ &&
              identical(m.element, findElement2.topFunction('f')) &&
              m.sourceRange.offset == code.position.offset &&
              m.sourceRange.length == '_'.length;
        }),
      ]),
    );
  }

  Future<void> test_searchReferences_parameter_wildcard() async {
    var code = '''
f(int _) {}
''';
    await resolveTestCode(code);

    var element = findElement2.parameter('_');
    var matches = await searchEngine.searchReferences(element);

    // No crashes.
    expect(matches, isEmpty);
  }

  Future<void>
  test_searchReferences_topFunction_parameter_optionalNamed_anywhere() async {
    var code = await resolveParsedCode('''
void foo(int a, int b, {int? test}) {}

void g() {
  foo(1, ^test: 0, 2);
}
''');

    var element = findElement2.parameter('test');
    var matches = await searchEngine.searchReferences(element);
    expect(
      matches,
      unorderedEquals([
        predicate((SearchMatch m) {
          return m.kind == MatchKind.REFERENCE_BY_NAMED_ARGUMENT &&
              identical(m.element, findElement2.topFunction('g')) &&
              m.sourceRange.offset == code.position.offset &&
              m.sourceRange.length == 'test'.length;
        }),
      ]),
    );
  }

  Future<void> test_searchReferences_underscoreField() async {
    var code = await resolveParsedCode('''
class A {
  final _ = 1;
  int a() => ^_;
}
''');

    var element = findElement2.field('_');
    var matches = await searchEngine.searchReferences(element);

    expect(
      matches,
      unorderedEquals([
        predicate((SearchMatch m) {
          return m.kind == MatchKind.READ &&
              identical(m.element, findElement2.method('a')) &&
              m.sourceRange.offset == code.position.offset &&
              m.sourceRange.length == '_'.length;
        }),
      ]),
    );
  }

  Future<void> test_searchReferences_underscoreTopLevelVariable() async {
    var code = await resolveParsedCode('''
final _ = 1;
int f() => ^_;
''');

    var element = findElement2.topVar('_');
    var matches = await searchEngine.searchReferences(element);

    expect(
      matches,
      unorderedEquals([
        predicate((SearchMatch m) {
          return m.kind == MatchKind.READ &&
              identical(m.element, findElement2.topFunction('f')) &&
              m.sourceRange.offset == code.position.offset &&
              m.sourceRange.length == '_'.length;
        }),
      ]),
    );
  }

  Future<void> test_searchTopLevelDeclarations() async {
    newFile('$testPackageLibPath/a.dart', '''
class A {}
int a;
''');

    newFile('$testPackageLibPath/b.dart', '''
class B {}
get b => 42;
''');

    await _ensureContainedFilesKnown();

    var matches = await searchEngine.searchTopLevelDeclarations('.*');
    matches.removeWhere((match) => match.libraryElement.isInSdk);
    expect(matches, hasLength(4));

    void assertHasOneElement(String name) {
      var nameMatches = matches.where(
        (SearchMatch m) =>
            m.kind == MatchKind.DECLARATION && m.element.name == name,
      );
      expect(nameMatches, hasLength(1));
    }

    assertHasOneElement('A');
    assertHasOneElement('a');
    assertHasOneElement('B');
    assertHasOneElement('b');
  }

  Future<void> test_searchTopLevelDeclarations_dependentPackage() async {
    var aaaRootPath = _configureForPackage_aaa();

    newFile('$aaaRootPath/lib/a.dart', '''
class A {}
''');

    // The `package:test` uses the class `A` from the `package:aaa`.
    // So it sees the declaration the element `A`.
    newFile(testFile.path, '''
import 'package:aaa/a.dart';
class B extends A {}
''');

    await _ensureContainedFilesKnown();

    var matches = await searchEngine.searchTopLevelDeclarations('.*');
    matches.removeWhere((match) => match.libraryElement.isInSdk);

    // We get exactly two items: `A` and `B`.
    // Specifically, we get exactly one `A`.
    expect(matches, hasLength(2));

    void assertHasOneElement(String name) {
      var nameMatches = matches.where(
        (SearchMatch m) =>
            m.kind == MatchKind.DECLARATION && m.element.name == name,
      );
      expect(nameMatches, hasLength(1));
    }

    assertHasOneElement('A');
    assertHasOneElement('B');
  }

  String _configureForPackage_aaa() {
    var aaaRootPath = '$workspaceRootPath/aaa';

    writePackageConfig(aaaRootPath, config: PackageConfigFileBuilder());

    writeTestPackageConfig(
      config: PackageConfigFileBuilder()
        ..add(name: 'aaa', rootFolder: getFolder(aaaRootPath)),
    );

    return aaaRootPath;
  }

  Future<void> _ensureContainedFilesKnown() async {
    for (var driver in allDrivers) {
      var contextRoot = driver.analysisContext!.contextRoot;
      for (var file in contextRoot.analyzedFiles()) {
        if (file.endsWith('.dart')) {
          await driver.getUnitElement(file);
        }
      }
    }
  }

  static void _assertContainsClass(
    Set<InterfaceElement> subtypes,
    String name,
  ) {
    expect(
      subtypes,
      contains(predicate((InterfaceElement e) => e.name == name)),
    );
  }
}

@reflectiveTest
class SearchEngineImplWithNonFunctionTypeAliasesTest
    extends SearchEngineImplTest {
  Future<void> test_searchReferences_typeAlias_interfaceType() async {
    await resolveTestCode('''
typedef A<T> = Map<T, String>;

void f(A<int> a, A<double> b) {}
''');

    var element = findElement2.typeAlias('A');
    var matches = await searchEngine.searchReferences(element);

    Matcher hasOne(Element element, String search) {
      return predicate((SearchMatch match) {
        return match.element == element &&
            match.sourceRange.offset == findNode.offset(search);
      });
    }

    expect(
      matches,
      unorderedMatches([
        hasOne(findElement2.parameter('a'), 'A<int>'),
        hasOne(findElement2.parameter('b'), 'A<double>'),
      ]),
    );
  }
}
