// 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 'dart:async';

import 'package:analysis_server/protocol/protocol.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';

main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(AnalysisHoverTest);
  });
}

@reflectiveTest
class AnalysisHoverTest extends AbstractAnalysisTest {
  Future<HoverInformation> prepareHover(String search) {
    int offset = findOffset(search);
    return prepareHoverAt(offset);
  }

  Future<HoverInformation> prepareHoverAt(int offset) async {
    await waitForTasksFinished();
    Request request =
        new AnalysisGetHoverParams(testFile, offset).toRequest('0');
    Response response = await waitResponse(request);
    var result = new AnalysisGetHoverResult.fromResponse(response);
    List<HoverInformation> hovers = result.hovers;
    return hovers.isNotEmpty ? hovers.first : null;
  }

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

  test_class() async {
    addTestFile('''
class A<E> {}
class I1<K, V> {}
class I2<E> {}
class M1 {}
class M2<E> {}
class B<T> extends A<T> with M1, M2<int> implements I1<int, String>, I2 {}
''');
    HoverInformation hover = await prepareHover('B<T>');
    expect(
        hover.elementDescription,
        'class B<T> extends A<T> with M1, M2<int> '
        'implements I1<int, String>, I2');
    expect(hover.staticType, isNull);
    expect(hover.propagatedType, isNull);
  }

  test_class_abstract() async {
    addTestFile('''
class A {}
abstract class B extends A {}
''');
    HoverInformation hover = await prepareHover('B extends');
    expect(hover.elementDescription, 'abstract class B extends A');
    expect(hover.staticType, isNull);
    expect(hover.propagatedType, isNull);
  }

  test_dartdoc_clunky() async {
    addTestFile('''
library my.library;
/**
 * doc aaa
 * doc bbb
 */
main() {
}
''');
    HoverInformation hover = await prepareHover('main() {');
    expect(hover.dartdoc, '''doc aaa\ndoc bbb''');
  }

  test_dartdoc_elegant() async {
    addTestFile('''
library my.library;
/// doc aaa
/// doc bbb
main() {
}
''');
    HoverInformation hover = await prepareHover('main() {');
    expect(hover.dartdoc, '''doc aaa\ndoc bbb''');
  }

  test_dartdoc_inherited_methodByMethod_fromInterface() async {
    addTestFile('''
class A {
  /// my doc
  m() {} // in A
}

class B implements A {
  m() {} // in B
}
''');
    HoverInformation hover = await prepareHover('m() {} // in B');
    expect(hover.dartdoc, '''my doc\n\nCopied from `A`.''');
  }

  test_dartdoc_inherited_methodByMethod_fromSuper_direct() async {
    addTestFile('''
class A {
  /// my doc
  m() {} // in A
}

class B extends A {
  m() {} // in B
}
''');
    HoverInformation hover = await prepareHover('m() {} // in B');
    expect(hover.dartdoc, '''my doc\n\nCopied from `A`.''');
  }

  test_dartdoc_inherited_methodByMethod_fromSuper_indirect() async {
    addTestFile('''
class A {
  /// my doc
  m() {}
}
class B extends A {
  m() {}
}
class C extends B {
  m() {} // in C
}''');
    HoverInformation hover = await prepareHover('m() {} // in C');
    expect(hover.dartdoc, '''my doc\n\nCopied from `A`.''');
  }

  test_dartdoc_inherited_methodByMethod_preferSuper() async {
    addTestFile('''
class A {
  /// my doc
  m() {}
}
class B extends A {
}
class I {
  // wrong doc
  m() {}
}
class C extends B implements I {
  m() {} // in C
}''');
    HoverInformation hover = await prepareHover('m() {} // in C');
    expect(hover.dartdoc, '''my doc\n\nCopied from `A`.''');
  }

  test_enum() async {
    addTestFile('''
enum MyEnum {AAA, BBB, CCC}
''');
    HoverInformation hover = await prepareHover('MyEnum');
    expect(hover.elementDescription, 'enum MyEnum');
    expect(hover.staticType, isNull);
    expect(hover.propagatedType, isNull);
  }

  test_expression_function() async {
    addTestFile('''
library my.library;
/// doc aaa
/// doc bbb
List<String> fff(int a, String b) {
}
''');
    HoverInformation hover = await prepareHover('fff(int a');
    // element
    expect(hover.containingLibraryName, 'my.library');
    expect(hover.containingLibraryPath, testFile);
    expect(hover.containingClassDescription, isNull);
    expect(hover.dartdoc, '''doc aaa\ndoc bbb''');
    expect(hover.elementDescription, 'fff(int a, String b) → List<String>');
    expect(hover.elementKind, 'function');
    // types
    expect(hover.staticType, isNull);
    expect(hover.propagatedType, isNull);
    // no parameter
    expect(hover.parameter, isNull);
  }

  test_expression_literal_noElement() async {
    addTestFile('''
main() {
  foo(123);
}
foo(Object myParameter) {}
''');
    HoverInformation hover = await prepareHover('123');
    // literal, no Element
    expect(hover.containingClassDescription, isNull);
    expect(hover.elementDescription, isNull);
    expect(hover.elementKind, isNull);
    // types
    expect(hover.staticType, 'int');
    expect(hover.propagatedType, isNull);
    // parameter
    expect(hover.parameter, 'Object myParameter');
  }

  test_expression_method() async {
    addTestFile('''
library my.library;
class A {
  /// doc aaa
  /// doc bbb
  List<String> mmm(int a, String b) {
  }
}
''');
    HoverInformation hover = await prepareHover('mmm(int a');
    // element
    expect(hover.containingLibraryName, 'my.library');
    expect(hover.containingLibraryPath, testFile);
    expect(hover.containingClassDescription, 'A');
    expect(hover.dartdoc, '''doc aaa\ndoc bbb''');
    expect(hover.elementDescription, 'mmm(int a, String b) → List<String>');
    expect(hover.elementKind, 'method');
    // types
    expect(hover.staticType, isNull);
    expect(hover.propagatedType, isNull);
    // no parameter
    expect(hover.parameter, isNull);
  }

  test_expression_method_deprecated() async {
    addTestFile('''
class A {
  @deprecated
  static void test() {}
}
main() {
  A.test();
}
''');
    HoverInformation hover = await prepareHover('test();');
    // element
    expect(hover.containingLibraryPath, testFile);
    expect(hover.elementDescription, 'test() → void');
    expect(hover.elementKind, 'method');
    expect(hover.isDeprecated, isTrue);
  }

  test_expression_method_invocation() async {
    addTestFile('''
library my.library;
class A {
  List<String> mmm(int a, String b) {
  }
}
main(A a) {
  a.mmm(42, 'foo');
}
''');
    HoverInformation hover = await prepareHover('mm(42, ');
    // range
    expect(hover.offset, findOffset('mmm(42, '));
    expect(hover.length, 'mmm'.length);
    // element
    expect(hover.containingLibraryName, 'my.library');
    expect(hover.containingLibraryPath, testFile);
    expect(hover.elementDescription, 'mmm(int a, String b) → List<String>');
    expect(hover.elementKind, 'method');
    expect(hover.isDeprecated, isFalse);
    // types
    expect(hover.staticType, '(int, String) → List<String>');
    expect(hover.propagatedType, isNull);
    // no parameter
    expect(hover.parameter, isNull);
  }

  test_expression_parameter() async {
    addTestFile('''
library my.library;
class A {
  /// The method documentation.
  m(int p) {
  }
}
''');
    HoverInformation hover = await prepareHover('p) {');
    // element
    expect(hover.containingLibraryName, isNull);
    expect(hover.containingLibraryPath, isNull);
    expect(hover.containingClassDescription, isNull);
    expect(hover.dartdoc, 'The method documentation.');
    expect(hover.elementDescription, 'int p');
    expect(hover.elementKind, 'parameter');
    // types
    expect(hover.staticType, 'int');
    expect(hover.propagatedType, isNull);
    // no parameter
    expect(hover.parameter, isNull);
  }

  test_expression_parameter_fieldFormal_declaration() async {
    addTestFile('''
class A {
  /// The field documentation.
  final int fff;
  A({this.fff});
}
main() {
  new A(fff: 42);
}
''');
    HoverInformation hover = await prepareHover('fff});');
    expect(hover.containingLibraryName, isNull);
    expect(hover.containingLibraryPath, isNull);
    expect(hover.containingClassDescription, isNull);
    expect(hover.dartdoc, 'The field documentation.');
    expect(hover.elementDescription, '{int fff}');
    expect(hover.elementKind, 'parameter');
    expect(hover.staticType, 'int');
  }

  test_expression_parameter_fieldFormal_use() async {
    addTestFile('''
class A {
  /// The field documentation.
  final int fff;
  A({this.fff});
}
main() {
  new A(fff: 42);
}
''');
    HoverInformation hover = await prepareHover('fff: 42');
    expect(hover.containingLibraryName, isNull);
    expect(hover.containingLibraryPath, isNull);
    expect(hover.containingClassDescription, isNull);
    expect(hover.dartdoc, 'The field documentation.');
    expect(hover.elementDescription, '{int fff}');
    expect(hover.elementKind, 'parameter');
    expect(hover.staticType, 'int');
  }

  test_expression_syntheticGetter_invocation() async {
    addTestFile('''
library my.library;
class A {
  /// doc aaa
  /// doc bbb
  String fff;
}
main(A a) {
  print(a.fff);
}
''');
    HoverInformation hover = await prepareHover('fff);');
    // element
    expect(hover.containingLibraryName, 'my.library');
    expect(hover.containingLibraryPath, testFile);
    expect(hover.containingClassDescription, 'A');
    expect(hover.dartdoc, '''doc aaa\ndoc bbb''');
    expect(hover.elementDescription, 'String fff');
    expect(hover.elementKind, 'field');
    // types
    expect(hover.staticType, 'String');
    expect(hover.propagatedType, isNull);
  }

  test_expression_variable_hasPropagatedType() async {
    addTestFile('''
library my.library;
main() {
  var vvv = 123;
  print(vvv);
}
''');
    HoverInformation hover = await prepareHover('vvv);');
    // element
    expect(hover.containingLibraryName, isNull);
    expect(hover.containingLibraryPath, isNull);
    expect(hover.containingClassDescription, isNull);
    expect(hover.dartdoc, isNull);
    expect(hover.elementDescription, 'dynamic vvv');
    expect(hover.elementKind, 'local variable');
    // types
    expect(hover.staticType, 'dynamic');
    expect(hover.propagatedType, 'int');
  }

  test_expression_variable_inMethod() async {
    addTestFile('''
library my.library;
class A {
  m() {
    num vvv = 42;
  }
}
''');
    HoverInformation hover = await prepareHover('vvv = 42');
    // element
    expect(hover.containingLibraryName, isNull);
    expect(hover.containingLibraryPath, isNull);
    expect(hover.containingClassDescription, isNull);
    expect(hover.dartdoc, isNull);
    expect(hover.elementDescription, 'num vvv');
    expect(hover.elementKind, 'local variable');
    // types
    expect(hover.staticType, 'num');
    expect(hover.propagatedType, 'int');
    // no parameter
    expect(hover.parameter, isNull);
  }

  test_instanceCreation_implicit() async {
    addTestFile('''
library my.library;
class A {
}
main() {
  new A();
}
''');
    HoverInformation hover = await prepareHover('new A');
    // range
    expect(hover.offset, findOffset('new A'));
    expect(hover.length, 'new A()'.length);
    // element
    expect(hover.containingLibraryName, 'my.library');
    expect(hover.containingLibraryPath, testFile);
    expect(hover.dartdoc, isNull);
    expect(hover.elementDescription, 'A() → A');
    expect(hover.elementKind, 'constructor');
    // types
    expect(hover.staticType, isNull);
    expect(hover.propagatedType, isNull);
    // no parameter
    expect(hover.parameter, isNull);
  }

  test_instanceCreation_implicit_withTypeArgument() async {
    addTestFile('''
library my.library;
class A<T> {}
main() {
  new A<String>();
}
''');
    void onConstructor(HoverInformation hover) {
      // range
      expect(hover.offset, findOffset('new A<String>'));
      expect(hover.length, 'new A<String>()'.length);
      // element
      expect(hover.containingLibraryName, 'my.library');
      expect(hover.containingLibraryPath, testFile);
      expect(hover.dartdoc, isNull);
      expect(hover.elementDescription, 'A() → A<String>');
      expect(hover.elementKind, 'constructor');
      // types
      expect(hover.staticType, isNull);
      expect(hover.propagatedType, isNull);
      // no parameter
      expect(hover.parameter, isNull);
    }

    {
      HoverInformation hover = await prepareHover('new A');
      onConstructor(hover);
    }
    {
      HoverInformation hover = await prepareHover('A<String>()');
      onConstructor(hover);
    }
    {
      HoverInformation hover = await prepareHover('String>');
      expect(hover.offset, findOffset('String>'));
      expect(hover.length, 'String'.length);
      expect(hover.elementKind, 'class');
    }
  }

  test_instanceCreation_named() async {
    addTestFile('''
library my.library;
class A {
  /// my doc
  A.named() {}
}
main() {
  new A.named();
}
''');
    void onConstructor(HoverInformation hover) {
      // range
      expect(hover.offset, findOffset('new A'));
      expect(hover.length, 'new A.named()'.length);
      // element
      expect(hover.dartdoc, 'my doc');
      expect(hover.elementDescription, 'A.named() → A');
      expect(hover.elementKind, 'constructor');
    }

    {
      HoverInformation hover = await prepareHover('new A');
      onConstructor(hover);
    }
    {
      HoverInformation hover = await prepareHover('named();');
      onConstructor(hover);
    }
  }

  test_noHoverInfo() async {
    addTestFile('''
library my.library;
main() {
  // nothing
}
''');
    HoverInformation hover = await prepareHover('nothing');
    expect(hover, isNull);
  }
}
