// 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:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/lint/constants.dart';
import 'package:analyzer/src/lint/linter.dart';
import 'package:analyzer/src/string_source.dart';
import 'package:analyzer/src/workspace/pub.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../../dart/resolution/context_collection_resolution.dart';

main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(CanBeConstConstructorTest);
    defineReflectiveTests(CanBeConstInstanceCreationTest);
    defineReflectiveTests(CanBeConstTypedLiteralTest);
    defineReflectiveTests(EvaluateExpressionTest);
    defineReflectiveTests(PubDependencyTest);
  });
}

@reflectiveTest
abstract class AbstractLinterContextTest extends PubPackageResolutionTest {
  late final LinterContextWithResolvedResults context;

  Future<void> resolve(String content) async {
    await resolveTestCode(content);
    var errorReporter = ErrorReporter(
      RecordingErrorListener(),
      StringSource(result.content, null),
    );
    var contextUnit = LintRuleUnitContext(
      file: result.file,
      content: result.content,
      errorReporter: errorReporter,
      unit: result.unit,
    );

    var libraryElement = result.libraryElement2;
    var analysisContext = libraryElement.session.analysisContext;
    var libraryPath = libraryElement.firstFragment.source.fullName;
    var workspace = analysisContext.contextRoot.workspace;
    var workspacePackage = workspace.findPackageFor(libraryPath);

    context = LinterContextWithResolvedResults(
      [contextUnit],
      contextUnit,
      result.typeProvider,
      result.typeSystem,
      InheritanceManager3(),
      // TODO(pq): Use a test package or consider passing in `null`.
      workspacePackage,
    );
  }
}

@reflectiveTest
class CanBeConstConstructorTest extends AbstractLinterContextTest {
  void assertCanBeConstConstructor(String search, bool expectedResult) {
    var constructor =
        findNode.constructor(search) as ConstructorDeclarationImpl;
    expect(constructor.canBeConst, expectedResult);
  }

  test_assertInitializer_parameter() async {
    await resolve(r'''
class C {
  C(int a) : assert(a >= 0, 'error');
}
''');
    assertCanBeConstConstructor('C(int a)', true);
  }

  test_empty() async {
    await resolve(r'''
class C {
  C();
}
''');
    assertCanBeConstConstructor('C()', true);
  }

  test_field_notConstInitializer() async {
    await resolve(r'''
class C {
  final int f = a;
  C();
}

var a = 0;
''');
    assertCanBeConstConstructor('C()', false);
  }

  test_field_notFinal() async {
    await resolve(r'''
class C {
  int f = 0;
  C();
}
''');
    assertCanBeConstConstructor('C()', false);
  }

  test_field_notFinal_inherited() async {
    await resolve(r'''
class A {
  int f = 0;
}

class B extends A {
  B();
}
''');
    assertCanBeConstConstructor('B()', false);
  }

  test_fieldInitializer_literal() async {
    await resolve(r'''
class C {
  final int f;
  C() : f = 0;
}
''');
    assertCanBeConstConstructor('C()', true);
  }

  test_fieldInitializer_notConst() async {
    await resolve(r'''
class C {
  final int f;
  C() : f = a;
}

var a = 0;
''');
    assertCanBeConstConstructor('C()', false);
  }

  test_fieldInitializer_parameter() async {
    await resolve(r'''
class C {
  final int f;
  C(int a) : f = a;
}
''');
    assertCanBeConstConstructor('C(int a)', true);
  }
}

@reflectiveTest
class CanBeConstInstanceCreationTest extends AbstractLinterContextTest {
  void assertCanBeConst(String snippet, bool expectedResult) {
    var node = findNode.instanceCreation(snippet);
    expect(node.canBeConst, expectedResult);
  }

  void test_deferred_argument() async {
    await resolveFileCode('$testPackageLibPath/a.dart', r'''
class A {
  const A();
}

const aa = A();
''');
    await resolve(r'''
import 'a.dart' deferred as a;

class B {
  const B(Object a);
}

main() {
  print(B(a.aa));
}
''');
    assertCanBeConst('B(a.aa)', false);
  }

  void test_false_argument_invocation() async {
    await resolve('''
class A {}
class B {
  const B(A a);
}
A f() => A();
B g() => B(f());
''');
    assertCanBeConst("B(f", false);
  }

  void test_false_argument_invocationInList() async {
    await resolve('''
class A {}
class B {
  const B(a);
}
A f() => A();
B g() => B([f()]);
''');
    assertCanBeConst("B([", false);
  }

  void test_false_argument_nonConstConstructor() async {
    await resolve('''
class A {}
class B {
  const B(A a);
}
B f() => B(A());
''');
    assertCanBeConst("B(A(", false);
  }

  void test_false_constructorReference_typeParameter() async {
    await resolve('''
class A<T> {
  const A();
}
class B<T> {
  final A<T> Function() fn;
  const B(this.fn);
}
B<T> fn<T>() => B(A<T>.new);
''');
    assertCanBeConst('B(A<T>.new)', false);
  }

  void test_false_mapKeyType_implementsEqual() async {
    await resolve('''
class A {
  const A();
  bool operator ==(other) => false;
}

class B {
  const B(_);
}

main() {
  B({A(): 0});
}
''');
    assertCanBeConst("B({", false);
  }

  void test_false_nonConstConstructor() async {
    await resolve('''
class A {}
A f() => A();
''');
    assertCanBeConst("A(", false);
  }

  void test_false_setElementType_implementsEqual() async {
    await resolve('''
class A {
  const A();
  bool operator ==(other) => false;
}

class B {
  const B(_);
}

main() {
  B({A()});
}
''');
    assertCanBeConst("B({", false);
  }

  void test_false_typeParameter() async {
    await resolve('''
class A<T> {
  const A();
}
f<U>() => A<U>();
''');
    assertCanBeConst("A<U>", false);
  }

  void test_true_argument_list_nonBool() async {
    await resolve('''
const bool kIsWeb = bool.fromEnvironment('dart.library.js_util');
class A {
  const A(List<int> l);
}
A f() => A([if (!kIsWeb) ...[1, 2, 3] else ...[1]]);
''');
    assertCanBeConst("A([", true);
  }

  void test_true_computeDependencies() async {
    newFile('$testPackageLibPath/a.dart', r'''
const a = 0;
''');

    await resolve('''
import 'a.dart';

class A {
  const A(int a);
}

A f() => A(a);
''');
    assertCanBeConst('A(a)', true);
  }

  void test_true_constConstructor_instance() async {
    newFile('$testPackageLibPath/a.dart', '''
class A {
  static const A instance = const A();
  const A();
}
''');
    await resolve('''
import 'a.dart';
class B {
  final A v;
  const B(this.v);
}
B f1() => B(A.instance);
''');
    assertCanBeConst('B(A.instance)', true);
  }

  void test_true_constConstructorArg() async {
    await resolve('''
class A {
  const A();
}
class B {
  const B(A a);
}
B f() => B(A());
''');
    assertCanBeConst("B(A(", true);
  }

  void test_true_constListArg() async {
    await resolve('''
class A {
  const A(List<int> l);
}
A f() => A([1, 2, 3]);
''');
    assertCanBeConst("A([", true);
  }

  void test_true_importedClass_defaultValue() async {
    var aPath = convertPath('$testPackageLibPath/a.dart');
    newFile(aPath, r'''
class A {
  final int a;
  const A({int b = 1}) : a = b * 2;
}
''');
    await resolve('''
import 'a.dart';

A f() => A();
''');
    assertCanBeConst("A();", true);
  }
}

@reflectiveTest
class CanBeConstTypedLiteralTest extends AbstractLinterContextTest {
  void assertCanBeConst(String snippet, bool expectedResult) {
    var node = findNode.typedLiteral(snippet);
    expect(node.canBeConst, expectedResult);
  }

  void test_listLiteral_false_forElement() async {
    await resolve('''
f() => [for (var i = 0; i < 10; i++) i];
''');
    assertCanBeConst('[for', false);
  }

  void test_listLiteral_false_methodInvocation() async {
    await resolve('''
f() => [g()];
int g() => 0;
''');
    assertCanBeConst('[', false);
  }

  void test_listLiteral_false_typeParameter() async {
    await resolve('''
class A<T> {
  const A();
}

f<U>() => [A<U>()];
''');
    assertCanBeConst('[', false);
  }

  void test_listLiteral_true_computeDependencies() async {
    newFile('$testPackageLibPath/a.dart', r'''
const a = 0;
''');

    await resolve('''
import 'a.dart';

f() => [a];
''');
    assertCanBeConst('[', true);
  }

  void test_listLiteral_true_constConstructor() async {
    await resolve('''
class A {
  const A();
}

f() => [A()];
''');
    assertCanBeConst('[', true);
  }

  void test_listLiteral_true_ifElement() async {
    await resolve('''
const a = true;
f() => [if (a) 0 else 1];
''');
    assertCanBeConst('[if', true);
  }

  void test_listLiteral_true_integerLiteral() async {
    await resolve('''
f() => [1, 2, 3];
''');
    assertCanBeConst('[', true);
  }

  void test_mapLiteral_false_forElement() async {
    await resolve('''
f() => {for (var i = 0; i < 10; i++) i: 0};
''');
    assertCanBeConst('{', false);
  }

  void test_mapLiteral_false_methodInvocation_key() async {
    await resolve('''
f() => {g(): 0};
int g() => 0;
''');
    assertCanBeConst('{', false);
  }

  void test_mapLiteral_false_methodInvocation_value() async {
    await resolve('''
f() => {0: g()};
int g() => 0;
''');
    assertCanBeConst('{', false);
  }

  void test_mapLiteral_true_ifElement() async {
    await resolve('''
const a = true;
f() => {if (a) 0: 0 else 1: 1};
''');
    assertCanBeConst('{', true);
  }

  void test_mapLiteral_true_integerLiteral() async {
    await resolve('''
f() => {1: 2, 3: 4};
''');
    assertCanBeConst('{', true);
  }

  void test_setLiteral_false_forElement() async {
    await resolve('''
f() => {for (var i = 0; i < 10; i++) i};
''');
    assertCanBeConst('{for', false);
  }

  void test_setLiteral_false_methodInvocation() async {
    await resolve('''
f() => {g()};
int g() => 0;
''');
    assertCanBeConst('{', false);
  }

  void test_setLiteral_true_ifElement() async {
    await resolve('''
const a = true;
f() => {if (a) 0 else 1};
''');
    assertCanBeConst('{', true);
  }

  void test_setLiteral_true_integerLiteral() async {
    await resolve('''
f() => {1, 2, 3};
''');
    assertCanBeConst('{', true);
  }
}

@reflectiveTest
class EvaluateExpressionTest extends AbstractLinterContextTest {
  test_hasError_listLiteral_forElement() async {
    await resolve('''
var x = const [for (var i = 0; i < 4; i++) i];
''');
    var result = _evaluateX();
    expect(result.errors, isNotEmpty);
    expect(result.value, isNull);
  }

  test_hasError_mapLiteral_forElement() async {
    await resolve('''
var x = const {for (var i = 0; i < 4; i++) i: 0};
''');
    var result = _evaluateX();
    expect(result.errors, isNotEmpty);
    expect(result.value, isNull);
  }

  test_hasError_methodInvocation() async {
    await resolve('''
var x = 42.abs();
''');
    var result = _evaluateX();
    expect(result.errors, isNotEmpty);
    expect(result.value, isNull);
  }

  test_hasError_setLiteral_forElement() async {
    await resolve('''
var x = const {for (var i = 0; i < 4; i++) i};
''');
    var result = _evaluateX();
    expect(result.errors, isNotEmpty);
    expect(result.value, isNull);
  }

  test_hasValue_binaryExpression() async {
    await resolve('''
var x = 1 + 2;
''');
    var result = _evaluateX();
    expect(result.errors, isEmpty);
    expect(result.value!.toIntValue(), 3);
  }

  test_hasValue_constantReference() async {
    await resolve('''
const a = 42;
var x = a;
''');
    var result = _evaluateX();
    expect(result.errors, isEmpty);
    expect(result.value!.toIntValue(), 42);
  }

  test_hasValue_constantReference_imported() async {
    newFile('$testPackageLibPath/a.dart', r'''
const a = 42;
''');
    await resolve('''
import 'a.dart';
var x = a;
''');
    var result = _evaluateX();
    expect(result.errors, isEmpty);
    expect(result.value!.toIntValue(), 42);
  }

  test_hasValue_intLiteral() async {
    await resolve('''
var x = 42;
''');
    var result = _evaluateX();
    expect(result.errors, isEmpty);
    expect(result.value!.toIntValue(), 42);
  }

  LinterConstantEvaluationResult _evaluateX() {
    var node = findNode.topVariableDeclarationByName('x').initializer!;
    return node.computeConstantValue();
  }
}

@reflectiveTest
class PubDependencyTest extends AbstractLinterContextTest {
  test_dependencies() async {
    newPubspecYamlFile(testPackageRootPath, '''
name: test

dependencies:
  args: '>=0.12.1 <2.0.0'
  charcode: ^1.1.0
''');
    await resolve(r'''
/// Dummy class.
class C { }
''');

    expect(context.package, TypeMatcher<PubPackage>());
    var pubPackage = context.package as PubPackage;
    var pubspec = pubPackage.pubspec!;

    var argsDep = pubspec.dependencies!
        .singleWhere((element) => element.name!.text == 'args');
    expect(argsDep.version!.value.text, '>=0.12.1 <2.0.0');

    var charCodeDep = pubspec.dependencies!
        .singleWhere((element) => element.name!.text == 'charcode');
    expect(charCodeDep.version!.value.text, '^1.1.0');
  }
}
