// 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:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

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

main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(ArgumentTypeNotAssignableTest);
    defineReflectiveTests(ArgumentTypeNotAssignableWithStrictCastsTest);
  });
}

@reflectiveTest
class ArgumentTypeNotAssignableTest extends PubPackageResolutionTest {
  test_ambiguousClassName() async {
    // See dartbug.com/19624
    newFile('$testPackageLibPath/lib2.dart', '''
class _A {}
g(h(_A a)) {}''');
    await assertErrorsInCode(
      '''
import 'lib2.dart';
class _A {}
f() {
  g((_A a) {});
}''',
      [error(diag.argumentTypeNotAssignable, 42, 9)],
    );
    // The name _A is private to the library it's defined in, so this is a type
    // mismatch. Furthermore, the error message should mention both _A and the
    // filenames so the user can figure out what's going on.
    String message = result.diagnostics[0].message;
    expect(message.contains("_A"), isTrue);
  }

  test_annotation_extensionType() async {
    await assertErrorsInCode(
      '''
extension type const A(String _) {}

@A(0)
void f() {}
''',
      [error(diag.argumentTypeNotAssignable, 40, 1)],
    );
  }

  test_annotation_namedConstructor() async {
    await assertErrorsInCode(
      '''
class A {
  const A.fromInt(int p);
}
@A.fromInt('0')
main() {}
''',
      [error(diag.argumentTypeNotAssignable, 49, 3)],
    );
  }

  test_annotation_namedConstructor_generic() async {
    await assertErrorsInCode(
      '''
class A<T> {
  const A.fromInt(T p);
}
@A<int>.fromInt('0')
main() {
}''',
      [error(diag.argumentTypeNotAssignable, 55, 3)],
    );
  }

  test_annotation_type_arguments_inferred() async {
    await assertNoErrorsInCode('''
@C([])
int i = 0;

class C<T> {
  const C(List<List<T>> arg);
}
''');
  }

  test_annotation_unnamedConstructor() async {
    await assertErrorsInCode(
      '''
class A {
  const A(int p);
}
@A('0')
main() {
}''',
      [error(diag.argumentTypeNotAssignable, 33, 3)],
    );
  }

  test_binary() async {
    await assertErrorsInCode(
      '''
class A {
  operator +(int p) {}
}
f(A a) {
  a + '0';
}''',
      [error(diag.argumentTypeNotAssignable, 50, 3)],
    );
  }

  test_binary_eqEq_covariantParameterType() async {
    await assertErrorsInCode(
      r'''
class A {
  bool operator==(covariant A other) => false;
}

void f(A a, A? aq) {
  a == 0;
  aq == 1;
  aq == aq;
  aq == null;
}
''',
      [
        error(diag.argumentTypeNotAssignable, 88, 1),
        error(diag.argumentTypeNotAssignable, 99, 1),
      ],
    );
  }

  test_call() async {
    await assertErrorsInCode(
      '''
typedef bool Predicate<T>(T object);

Predicate<String> f() => (String s) => false;

void main() {
  f().call(3);
}''',
      [error(diag.argumentTypeNotAssignable, 110, 1)],
    );
  }

  test_cascadeSecond() async {
    await assertErrorsInCode(
      '''
// filler filler filler filler filler filler filler filler filler filler
class A {
  B ma() { return new B(); }
}
class B {
  mb(String p) {}
}

main() {
  A a = new A();
  a..  ma().mb(0);
}''',
      [error(diag.argumentTypeNotAssignable, 186, 1)],
    );
  }

  test_const() async {
    await assertErrorsInCode(
      '''
class A {
  const A(String p);
}
main() {
  const A(42);
}''',
      [
        error(diag.argumentTypeNotAssignable, 52, 2),
        error(diag.constConstructorParamTypeMismatch, 52, 2),
      ],
    );
  }

  test_const_super() async {
    await assertErrorsInCode(
      '''
class A {
  const A(String p);
}
class B extends A {
  const B() : super(42);
}''',
      [error(diag.argumentTypeNotAssignable, 73, 2)],
    );
  }

  test_downcast() async {
    await assertErrorsInCode(
      r'''
m() {
  num y = 1;
  n(y);
}
n(int x) {}
''',
      [error(diag.argumentTypeNotAssignable, 23, 1)],
    );
  }

  test_downcast_nullableNonNullable() async {
    await assertErrorsInCode(
      r'''
m() {
  int? y;
  n(y);
}
n(int x) {}
''',
      [error(diag.argumentTypeNotAssignable, 20, 1)],
    );
  }

  test_dynamicCast() async {
    await assertNoErrorsInCode(r'''
m() {
  dynamic i;
  n(i);
}
n(int i) {}
''');
  }

  test_enumConstant() async {
    await assertErrorsInCode(
      r'''
enum E {
  v(0);
  const E(String a);
}
''',
      [
        error(diag.argumentTypeNotAssignable, 13, 1),
        error(diag.constConstructorParamTypeMismatch, 13, 1),
      ],
    );
  }

  test_enumConstant_implicitDouble() async {
    await assertNoErrorsInCode(r'''
enum E {
  v(0);
  const E(double a);
}
''');
  }

  test_expressionFromConstructorTearoff_withoutTypeArgs() async {
    await assertNoErrorsInCode('''
class C<T> {
  C(T a);
}

var g = C.new;
var x = g('Hello');
''');
  }

  test_expressionFromConstructorTearoff_withTypeArgs_assignable() async {
    await assertNoErrorsInCode('''
class C<T> {
  C(T a);
}

var g = C<int>.new;
var x = g(0);
''');
  }

  test_expressionFromConstructorTearoff_withTypeArgs_notAssignable() async {
    await assertErrorsInCode(
      '''
class C<T> {
  C(T a);
}

var g = C<int>.new;
var x = g('Hello');
''',
      [error(diag.argumentTypeNotAssignable, 56, 7)],
    );
  }

  test_expressionFromFunctionTearoff_withoutTypeArgs() async {
    await assertNoErrorsInCode('''
void f<T>(T a) {}

var g = f;
var x = g('Hello');
''');
  }

  test_expressionFromFunctionTearoff_withTypeArgs_assignable() async {
    await assertNoErrorsInCode('''
void f<T>(T a) {}

var g = f<int>;
var x = g(0);
''');
  }

  test_expressionFromFunctionTearoff_withTypeArgs_notAssignable() async {
    await assertErrorsInCode(
      '''
void f<T>(T a) {}

var g = f<int>;
var x = g('Hello');
''',
      [error(diag.argumentTypeNotAssignable, 45, 7)],
    );
  }

  test_for_element_type_inferred_from_rewritten_node() async {
    // See https://github.com/dart-lang/sdk/issues/39171
    await assertNoErrorsInCode('''
void f<T>(Iterable<T> Function() g, int Function(T) h) {
  [for (var x in g()) if (x is String) h(x)];
}
''');
  }

  test_for_statement_type_inferred_from_rewritten_node() async {
    // See https://github.com/dart-lang/sdk/issues/39171
    await assertNoErrorsInCode('''
void f<T>(Iterable<T> Function() g, void Function(T) h) {
  for (var x in g()) {
    if (x is String) {
      h(x);
    }
  }
}
''');
  }

  test_functionExpressionInvocation_required() async {
    await assertErrorsInCode(
      '''
main() {
  (int x) {} ('');
}''',
      [error(diag.argumentTypeNotAssignable, 23, 2)],
    );
  }

  test_functionType() async {
    await assertErrorsInCode(
      r'''
m() {
  var a = new A();
  a.n(() => 0);
}
class A {
  n(void f(int i)) {}
}
''',
      [error(diag.argumentTypeNotAssignable, 31, 7)],
    );
  }

  test_implicitCallReference() async {
    await assertNoErrorsInCode('''
class A {
  void call(int p) {}
}
void f(void Function(int) a) {}
void g(A a) {
  f(a);
}
''');
  }

  test_implicitCallReference_named() async {
    await assertNoErrorsInCode('''
class A {
  void call(int p) {}
}
void defaultFunc(int p) {}
void f({void Function(int) a = defaultFunc}) {}
void g(A a) {
  f(a: a);
}
''');
  }

  test_implicitCallReference_namedAndRequired() async {
    await assertNoErrorsInCode('''
class A {
  void call(int p) {}
}
void f({required void Function(int) a}) {}
void g(A a) {
  f(a: a);
}
''');
  }

  test_implicitCallReference_this() async {
    await assertNoErrorsInCode('''
class A {
  void call(int p) {}

  void f(void Function(int) a) {}
  void g() {
    f(this);
  }
}
''');
  }

  test_index_invalidRead() async {
    await assertErrorsInCode(
      '''
class A {
  int operator [](int index) => 0;
}
f(A a) {
  a['0'];
}''',
      [error(diag.argumentTypeNotAssignable, 60, 3)],
    );
  }

  test_index_invalidRead_validWrite() async {
    await assertErrorsInCode(
      '''
class A {
  int operator [](int index) => 0;
  operator []=(String index, int value) {}
}
f(A a) {
  a['0'] += 0;
  ++a['0'];
  a['0']++;
}''',
      [
        error(diag.argumentTypeNotAssignable, 103, 3),
        error(diag.argumentTypeNotAssignable, 120, 3),
        error(diag.argumentTypeNotAssignable, 130, 3),
      ],
    );
  }

  test_index_invalidWrite() async {
    await assertErrorsInCode(
      '''
class A {
  operator []=(int index, int value) {}
}
f(A a) {
  a['0'] = 0;
}''',
      [error(diag.argumentTypeNotAssignable, 65, 3)],
    );
  }

  test_index_validRead_invalidWrite() async {
    await assertErrorsInCode(
      '''
class A {
  int operator [](String index) => 0;
  operator []=(int index, int value) {}
}
f(A a) {
  a['0'] += 0;
  ++a['0'];
  a['0']++;
}''',
      [
        error(diag.argumentTypeNotAssignable, 103, 3),
        error(diag.argumentTypeNotAssignable, 120, 3),
        error(diag.argumentTypeNotAssignable, 130, 3),
      ],
    );
  }

  test_interfaceType() async {
    await assertErrorsInCode(
      r'''
m() {
  var i = '';
  n(i);
}
n(int i) {}
''',
      [error(diag.argumentTypeNotAssignable, 24, 1)],
    );
  }

  test_invocation_callParameter() async {
    await assertErrorsInCode(
      '''
class A {
  call(int p) {}
}
f(A a) {
  a('0');
}''',
      [error(diag.argumentTypeNotAssignable, 42, 3)],
    );
  }

  test_invocation_callVariable() async {
    await assertErrorsInCode(
      '''
class A {
  call(int p) {}
}
main() {
  A a = new A();
  a('0');
}''',
      [error(diag.argumentTypeNotAssignable, 59, 3)],
    );
  }

  test_invocation_functionParameter() async {
    await assertErrorsInCode(
      '''
a(b(int p)) {
  b('0');
}''',
      [error(diag.argumentTypeNotAssignable, 18, 3)],
    );
  }

  test_invocation_functionParameter_generic() async {
    await assertErrorsInCode(
      '''
class A<K, V> {
  m(f(K k), V v) {
    f(v);
  }
}''',
      [error(diag.argumentTypeNotAssignable, 41, 1)],
    );
  }

  test_invocation_functionTypes_optional() async {
    await assertErrorsInCode(
      '''
void acceptFunOptBool(void funNumOptBool([bool b])) {}
void funBool(bool b) {}
main() {
  acceptFunOptBool(funBool);
}''',
      [error(diag.argumentTypeNotAssignable, 107, 7)],
    );
  }

  test_invocation_functionTypes_optional_method() async {
    await assertErrorsInCode(
      '''
void acceptFunOptBool(void funOptBool([bool b])) {}
class C {
  static void funBool(bool b) {}
}
main() {
  acceptFunOptBool(C.funBool);
}''',
      [error(diag.argumentTypeNotAssignable, 125, 9)],
    );
  }

  test_invocation_generic() async {
    await assertErrorsInCode(
      '''
class A<T> {
  m(T t) {}
}
f(A<String> a) {
  a.m(1);
}''',
      [error(diag.argumentTypeNotAssignable, 50, 1)],
    );
  }

  test_invocation_named() async {
    await assertErrorsInCode(
      '''
f({String p = ''}) {}
main() {
  f(p: 42);
}''',
      [error(diag.argumentTypeNotAssignable, 38, 2)],
    );
  }

  test_invocation_optional() async {
    await assertErrorsInCode(
      '''
f([String p = '']) {}
main() {
  f(42);
}''',
      [error(diag.argumentTypeNotAssignable, 35, 2)],
    );
  }

  test_invocation_required() async {
    await assertErrorsInCode(
      '''
f(String p) {}
main() {
  f(42);
}''',
      [error(diag.argumentTypeNotAssignable, 28, 2)],
    );
  }

  test_invocation_typedef_generic() async {
    await assertErrorsInCode(
      '''
typedef A<T>(T p);
f(A<int> a) {
  a('1');
}''',
      [error(diag.argumentTypeNotAssignable, 37, 3)],
    );
  }

  test_invocation_typedef_local() async {
    await assertErrorsInCode(
      '''
typedef A(int p);
A getA() => throw '';
main() {
  A a = getA();
  a('1');
}''',
      [error(diag.argumentTypeNotAssignable, 69, 3)],
    );
  }

  test_invocation_typedef_parameter() async {
    await assertErrorsInCode(
      '''
typedef A(int p);
f(A a) {
  a('1');
}''',
      [error(diag.argumentTypeNotAssignable, 31, 3)],
    );
  }

  test_map_indexGet() async {
    // Any type may be passed to Map.operator[].
    await assertNoErrorsInCode('''
main() {
  Map<int, int> m = <int, int>{};
  m['x'];
}
''');
  }

  test_map_indexSet() async {
    // The type passed to Map.operator[]= must match the key type.
    await assertErrorsInCode(
      '''
main() {
  Map<int, int> m = <int, int>{};
  m['x'] = 0;
}
''',
      [error(diag.argumentTypeNotAssignable, 47, 3)],
    );
  }

  test_map_indexSet_ifNull() async {
    // The type passed to Map.operator[]= must match the key type.
    await assertErrorsInCode(
      '''
main() {
  Map<int, int> m = <int, int>{};
  m['x'] ??= 0;
}
''',
      [error(diag.argumentTypeNotAssignable, 47, 3)],
    );
  }

  test_new_generic() async {
    await assertErrorsInCode(
      '''
class A<T> {
  A(T p) {}
}
main() {
  new A<String>(42);
}''',
      [error(diag.argumentTypeNotAssignable, 52, 2)],
    );
  }

  test_new_optional() async {
    await assertErrorsInCode(
      '''
class A {
  A([String p = '']) {}
}
main() {
  new A(42);
}''',
      [error(diag.argumentTypeNotAssignable, 53, 2)],
    );
  }

  test_new_required() async {
    await assertErrorsInCode(
      '''
class A {
  A(String p) {}
}
main() {
  new A(42);
}''',
      [error(diag.argumentTypeNotAssignable, 46, 2)],
    );
  }

  void test_recordType() async {
    await assertErrorsInCode(
      '''
void f((int a, int b) r) {}

void g() {
  f((a: 1, b: 2));
}
''',
      [
        error(
          diag.argumentTypeNotAssignable,
          44,
          12,
          messageContains: [
            'Expected 2 positional arguments, but got 0 instead.',
          ],
        ),
      ],
    );
  }

  void test_recordType_namedArguments() async {
    await assertErrorsInCode(
      '''
typedef A = ({
  int b,
  int c,
});

void f(A a){print(a);}

main() {
 f((bb:2, c:3));
}
''',
      [
        error(
          diag.argumentTypeNotAssignable,
          74,
          11,
          messageContains: ['Unexpected named argument `bb` with type `int`.'],
        ),
      ],
    );
  }

  void test_recordType_namedArguments_missing() async {
    await assertErrorsInCode(
      '''
typedef A = ({
  int b,
  int c,
});

void f(A a){print(a);}

main() {
 f((b:2));
}
''',
      [
        error(
          diag.argumentTypeNotAssignable,
          74,
          5,
          messageContains: ['Expected 2 named arguments, but got 1 instead.'],
        ),
      ],
    );
  }

  void test_recordType_positionalArguments() async {
    await assertErrorsInCode(
      '''
typedef A = (
  int b,
  int c,
);

void f(A a){print(a);}

main() {
 f((3, 2, 1));
}
''',
      [
        error(
          diag.argumentTypeNotAssignable,
          72,
          9,
          messageContains: [
            'Expected 2 positional arguments, but got 3 instead.',
          ],
        ),
      ],
    );
  }

  @failingTest
  test_tearOff_required() async {
    await assertErrorsInCode(
      '''
class C {
  Object/*=T*/ f/*<T>*/(Object/*=T*/ x) => x;
}
g(C c) {
  var h = c.f/*<int>*/;
  print(h('s'));
}
''',
      [error(diag.argumentTypeNotAssignable, 99, 1)],
    );
  }
}

@reflectiveTest
class ArgumentTypeNotAssignableWithStrictCastsTest
    extends PubPackageResolutionTest
    with WithStrictCastsMixin {
  test_extensionTypePrimaryConstructor() async {
    await assertErrorsWithStrictCasts(
      '''
extension type E(int i) {}

dynamic a;
var e = E(a);
''',
      [error(diag.argumentTypeNotAssignable, 49, 1)],
    );
  }

  test_functionCall() async {
    await assertErrorsWithStrictCasts(
      '''
void f(int i) {}
void foo(dynamic a) {
  f(a);
}
''',
      [error(diag.argumentTypeNotAssignable, 43, 1)],
    );
  }

  test_operator() async {
    await assertErrorsWithStrictCasts(
      '''
void foo(int i, dynamic a) {
  i + a;
}
''',
      [error(diag.argumentTypeNotAssignable, 35, 1)],
    );
  }
}
