// Copyright (c) 2014, 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/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_constants.dart';
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../analysis_abstract.dart';
import '../analysis_server_base.dart';

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

@reflectiveTest
class AnalysisNotificationImplementedTest extends PubPackageAnalysisServerTest {
  List<ImplementedClass>? implementedClasses;
  List<ImplementedMember>? implementedMembers;

  /// Validates that there is an [ImplementedClass] at the offset of [search].
  ///
  /// If [length] is not specified explicitly, then length of an identifier
  /// from [search] is used.
  void assertHasImplementedClass(String search, [int length = -1]) {
    var offset = findOffset(search);
    if (length == -1) {
      length = findIdentifierLength(search);
    }
    if (implementedClasses == null) {
      fail('No notification of implemented classes was received');
    }
    for (var clazz in implementedClasses!) {
      if (clazz.offset == offset && clazz.length == length) {
        return;
      }
    }
    fail(
      'Expect to find an implemented class at $offset'
      ' in $implementedClasses',
    );
  }

  /// Validates that there is an [ImplementedClass] at the offset of [search].
  ///
  /// If [length] is not specified explicitly, then length of an identifier
  /// from [search] is used.
  void assertHasImplementedMember(String search, [int length = -1]) {
    var offset = findOffset(search);
    if (length == -1) {
      length = findIdentifierLength(search);
    }
    if (implementedMembers == null) {
      fail('No notification of implemented members was received');
    }
    for (var member in implementedMembers!) {
      if (member.offset == offset && member.length == length) {
        return;
      }
    }
    fail(
      'Expect to find an implemented member at $offset'
      ' in $implementedMembers',
    );
  }

  /// Validates that there is no [ImplementedMember] at the offset of [search].
  ///
  /// If [length] is not specified explicitly, then length of an identifier
  /// from [search] is used.
  void assertNoImplementedMember(String search, [int length = -1]) {
    var offset = findOffset(search);
    if (length == -1) {
      length = findIdentifierLength(search);
    }
    if (implementedMembers == null) {
      fail('No notification of implemented members was received');
    }
    for (var member in implementedMembers!) {
      if (member.offset == offset) {
        fail(
          'Unexpected implemented member at $offset'
          ' in $implementedMembers',
        );
      }
    }
  }

  /// Subscribe for `IMPLEMENTED` and wait for the notification.
  Future<void> prepareImplementedElements() async {
    await subscribeForImplemented();
    return waitForImplementedElements();
  }

  @override
  void processNotification(Notification notification) {
    if (notification.event == analysisNotificationImplemented) {
      var params = AnalysisImplementedParams.fromNotification(
        notification,
        clientUriConverter: server.uriConverter,
      );
      if (params.file == testFile.path) {
        implementedClasses = params.classes;
        implementedMembers = params.members;
      }
    }
  }

  @override
  Future<void> setUp() async {
    super.setUp();
    await setRoots(included: [workspaceRootPath], excluded: []);
  }

  Future<void> subscribeForImplemented() async {
    setPriorityFiles([testFile]);
    await addAnalysisSubscription(AnalysisService.IMPLEMENTED, testFile);
  }

  Future<void> test_afterAnalysis() async {
    addTestFile('''
class A {}
class B extends A {}
''');
    await waitForTasksFinished();
    await prepareImplementedElements();
    assertHasImplementedClass('A {');
  }

  Future<void> test_class_extended() async {
    addTestFile('''
class A {}
class B extends A {}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('A {');
  }

  @SkippedTest() // TODO(scheglov): implement augmentation
  Future<void> test_class_extended_augment() async {
    newFile('$testPackageLibPath/b.dart', '''
part 'test.dart';

class A {}
class B extends A {}
''');
    addTestFile('''
part of 'b.dart';

augment class A {} // 1
augment class A {} // 2
''');
    await prepareImplementedElements();
    assertHasImplementedClass('A {} // 1');
    assertHasImplementedClass('A {} // 2');
  }

  Future<void> test_class_extended_inAugmentation() async {
    newFile('$testPackageLibPath/a.dart', '''
part of 'test.dart';

class B extends A {}
''');
    addTestFile('''
part 'a.dart';

class A {}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('A {');
  }

  Future<void> test_class_extended_inAugmented() async {
    newFile('$testPackageLibPath/b.dart', '''
part 'test.dart';

class B extends A {}
''');
    addTestFile('''
part of 'b.dart';

class A {}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('A {');
  }

  Future<void> test_class_implementedBy_class() async {
    addTestFile('''
class A {}
class B implements A {}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('A {');
  }

  Future<void> test_class_implementedBy_enum() async {
    addTestFile('''
class A {}

enum E implements A {
  v
}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('A {');
  }

  Future<void> test_class_implementedBy_enum_getterByGetter() async {
    addTestFile('''
class A {
  int get foo => 0; // A
}

enum E implements A {
  v;
  int get foo => 0; // E
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('foo => 0; // A');
    assertNoImplementedMember('foo => 0; // E');
  }

  Future<void> test_class_implementedBy_enum_methodByMethod() async {
    addTestFile('''
class A {
  void foo() {} // A
}

enum E implements A {
  v;
  void foo() {} // E
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('foo() {} // A');
    assertNoImplementedMember('foo() {} // E');
  }

  Future<void> test_class_implementedBy_enum_setterBySetter() async {
    addTestFile('''
class A {
  set foo(int _) {} // A
}

enum E implements A {
  v;
  set foo(int _) {} // E
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('foo(int _) {} // A');
    assertNoImplementedMember('foo(int _) {} // E');
  }

  Future<void> test_class_implementedBy_extensionType() async {
    addTestFile('''
class A {}
extension type B(A it) implements A {}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('A {');
  }

  Future<void> test_class_implementedBy_mixin() async {
    addTestFile('''
class A {} // ref
class B {} // ref
class C {} // ref
class D {} // ref
mixin M on A, B implements C, D {}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('A {} // ref');
    assertHasImplementedClass('B {} // ref');
    assertHasImplementedClass('C {} // ref');
    assertHasImplementedClass('D {} // ref');
  }

  Future<void> test_class_mixedBy_class() async {
    addTestFile('''
class A {}
class B = Object with A;
''');
    await prepareImplementedElements();
    assertHasImplementedClass('A {');
  }

  Future<void> test_class_mixedBy_enum() async {
    addTestFile('''
mixin M {}
enum E with M {
  v
}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('M {}');
  }

  Future<void> test_class_mixedBy_enum_methodByMethod() async {
    addTestFile('''
class M {
  void foo() {} // M
}

enum E with M {
  v;
  void foo() {} // E
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('foo() {} // M');
    assertNoImplementedMember('foo() {} // E');
  }

  Future<void> test_extensionType_implementedBy_extensionType() async {
    addTestFile('''
extension type A(int it) {}
extension type B(int it) implements A {}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('A(int it)');
  }

  Future<void> test_mixin_implemented() async {
    addTestFile('''
mixin M { // ref
  void foo() {} // ref
  void bar() {} // ref
}

class A implements M {
  void foo() {}
}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('M { // ref');
    assertHasImplementedMember('foo() {} // ref');
    assertNoImplementedMember('bar() {} // ref');
  }

  Future<void> test_mixin_mixed() async {
    addTestFile('''
mixin M { // ref
  void foo() {} // ref
  void bar() {} // ref
}

class A extends Object with M {
  void foo() {}
}
''');
    await prepareImplementedElements();
    assertHasImplementedClass('M { // ref');
    assertHasImplementedMember('foo() {} // ref');
    assertNoImplementedMember('bar() {} // ref');
  }

  Future<void> test_ofClass_byClass_field_withField() async {
    addTestFile('''
class A {
  int f; // A
}
class B extends A {
  int f;
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('f; // A');
  }

  Future<void> test_ofClass_byClass_field_withGetter() async {
    addTestFile('''
class A {
  int f; // A
}
class B extends A {
  get f => null;
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('f; // A');
  }

  Future<void> test_ofClass_byClass_field_withSetter() async {
    addTestFile('''
class A {
  int f; // A
}
class B extends A {
  void set f(_) {}
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('f; // A');
  }

  Future<void> test_ofClass_byClass_getter_withField() async {
    addTestFile('''
class A {
  get f => null; // A
}
class B extends A {
  int f;
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('f => null; // A');
  }

  Future<void> test_ofClass_byClass_getter_withGetter() async {
    addTestFile('''
class A {
  get f => null; // A
}
class B extends A {
  get f => null;
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('f => null; // A');
  }

  @SkippedTest() // TODO(scheglov): implement augmentation
  Future<void> test_ofClass_byClass_method_augment() async {
    newFile('$testPackageLibPath/b.dart', '''
part 'test.dart';

class A {
  m() {}
}
class B extends A {
  m() {}
}
''');
    addTestFile('''
part of 'b.dart';

augment class A {
  augment m() {} // 1
  augment m() {} // 2
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('m() {} // 1');
    assertHasImplementedMember('m() {} // 2');
  }

  @SkippedTest() // TODO(scheglov): implement augmentation
  Future<void> test_ofClass_byClass_method_inAugmentation() async {
    newFile('$testPackageLibPath/a.dart', '''
part of 'test.dart';

augment class B extends A {
  m() {}
}
''');
    addTestFile('''
part 'a.dart';

class A {
  m() {}
}
class B extends A {}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('m()');
  }

  Future<void> test_ofClass_byClass_method_inAugmented() async {
    newFile('$testPackageLibPath/b.dart', '''
part 'test.dart';

class A {}
class B extends A {
  m() {}
}
''');
    addTestFile('''
part of 'b.dart';

augment class A {
  m() {}
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('m()');
  }

  Future<void> test_ofClass_byClass_method_withMethod() async {
    addTestFile('''
class A {
  m() {} // A
}
class B extends A {
  m() {} // B
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('m() {} // A');
    assertNoImplementedMember('m() {} // B');
  }

  Future<void> test_ofClass_byClass_method_withMethod_indirectSubclass() async {
    addTestFile('''
class A {
  m() {} // A
}
class B extends A {
}
class C extends A {
  m() {}
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('m() {} // A');
  }

  Future<void>
  test_ofClass_byClass_method_withMethod_private_differentLib() async {
    newFile('$testPackageLibPath/lib.dart', r'''
import 'test.dart';
class B extends A {
  void _m() {}
}
''');
    addTestFile('''
class A {
  _m() {} // A
}
''');
    await prepareImplementedElements();
    assertNoImplementedMember('_m() {} // A');
  }

  Future<void>
  test_ofClass_byClass_method_withMethod_private_sameLibrary() async {
    addTestFile('''
class A {
  _m() {} // A
}
class B extends A {
  _m() {} // B
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('_m() {} // A');
    assertNoImplementedMember('_m() {} // B');
  }

  Future<void> test_ofClass_byClass_method_withMethod_wasAbstract() async {
    addTestFile('''
abstract class A {
  m(); // A
}
class B extends A {
  m() {}
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('m(); // A');
  }

  Future<void> test_ofClass_byClass_setter_withField() async {
    addTestFile('''
class A {
  set f(_) {} // A
}
class B extends A {
  int f;
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('f(_) {} // A');
  }

  Future<void> test_ofClass_byClass_setter_withSetter() async {
    addTestFile('''
class A {
  set f(_) {} // A
}
class B extends A {
  set f(_) {} // B
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('f(_) {} // A');
  }

  Future<void> test_ofClass_byClass_static_field_instanceStatic() async {
    addTestFile('''
class A {
  int F = 0;
}
class B extends A {
  static int F = 1;
}
''');
    await prepareImplementedElements();
    assertNoImplementedMember('F = 0');
  }

  Future<void> test_ofClass_byClass_static_field_staticInstance() async {
    addTestFile('''
class A {
  static int F = 0;
}
class B extends A {
  int F = 1;
}
''');
    await prepareImplementedElements();
    assertNoImplementedMember('F = 0');
  }

  Future<void> test_ofClass_byClass_static_field_staticStatic() async {
    addTestFile('''
class A {
  static int F = 0;
}
class B extends A {
  static int F = 1;
}
''');
    await prepareImplementedElements();
    assertNoImplementedMember('F = 0');
  }

  Future<void> test_ofClass_byClass_static_method_instanceStatic() async {
    addTestFile('''
class A {
  int m() => 0;
}
class B extends A {
  static int m() => 1;
}
''');
    await prepareImplementedElements();
    assertNoImplementedMember('m() => 0');
  }

  Future<void> test_ofClass_byClass_static_method_staticInstance() async {
    addTestFile('''
class A {
  static int m() => 0;
}
class B extends A {
  int m() => 1;
}
''');
    await prepareImplementedElements();
    assertNoImplementedMember('m() => 0');
  }

  Future<void> test_ofClass_byClass_static_method_staticStatic() async {
    addTestFile('''
class A {
  static int m() => 0;
}
class B extends A {
  static int m() => 1;
}
''');
    await prepareImplementedElements();
    assertNoImplementedMember('m() => 0');
  }

  Future<void> test_ofClass_byExtensionType_method_withMethod() async {
    addTestFile('''
class A {
  void foo() {} // A
}
extension type E(A it) implements A {
  void foo() {} // B
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('foo() {} // A');
    assertNoImplementedMember('foo() {} // B');
  }

  Future<void> test_ofExtensionType_method_withMethod() async {
    addTestFile('''
extension type A(int) {
  void foo() {} // A
}
extension type B(int) implements A {
  void foo() {} // B
}
''');
    await prepareImplementedElements();
    assertHasImplementedMember('foo() {} // A');
    assertNoImplementedMember('foo() {} // B');
  }

  Future<void> waitForImplementedElements() {
    Future<void> waitForNotification(int times) {
      if (times == 0 || implementedClasses != null) {
        return Future.value();
      }
      return Future.delayed(
        Duration(milliseconds: 1),
        () => waitForNotification(times - 1),
      );
    }

    return waitForNotification(30000);
  }
}
