// 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:analyzer_testing/analysis_rule/analysis_rule.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(ConstConstructorFieldTypeMismatchContextTest);
    defineReflectiveTests(ConstEvalThrowsExceptionTest);
  });
}

@reflectiveTest
class ConstConstructorFieldTypeMismatchContextTest
    extends PubPackageResolutionTest {
  test_generic_string_int() async {
    await assertErrorsInCode(
      r'''
class C<T> {
  final T x = y;
  const C();
}
const int y = 1;
var v = const C<String>();
''',
      [
        error(diag.invalidAssignment, 27, 1),
        error(
          diag.constEvalThrowsException,
          70,
          17,
          contextMessages: [
            contextMessage(
              testFile,
              27,
              1,
              textContains: [
                "The exception is 'In a const constructor, a value of type 'int' can't be assigned to the field 'x', which has type 'String'.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_notGeneric_int_int() async {
    await assertErrorsInCode(
      r'''
class A {
  const A(x) : y = x;
  final int y;
}
var v = const A('foo');
''',
      [
        error(
          diag.constEvalThrowsException,
          57,
          14,
          contextMessages: [
            contextMessage(
              testFile,
              29,
              1,
              textContains: [
                "The exception is 'In a const constructor, a value of type 'String' can't be assigned to the field 'y', which has type 'int'.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_notGeneric_int_null() async {
    var errors = [
      error(
        diag.constEvalThrowsException,
        57,
        13,
        contextMessages: [
          contextMessage(
            testFile,
            29,
            1,
            textContains: [
              "The exception is 'In a const constructor, a value of type 'Null' can't be assigned to the field 'y', which has type 'int'.' and occurs here.",
            ],
          ),
        ],
      ),
    ];
    await assertErrorsInCode(r'''
class A {
  const A(x) : y = x;
  final int y;
}
var v = const A(null);
''', errors);
  }

  test_notGeneric_null_forNonNullable_fromNullSafe() async {
    await assertErrorsInCode(
      '''
class C {
  final int f;
  const C(a) : f = a;
}

const a = const C(null);
''',
      [
        error(
          diag.constEvalThrowsException,
          60,
          13,
          contextMessages: [
            contextMessage(
              testFile,
              44,
              1,
              textContains: [
                "The exception is 'In a const constructor, a value of type 'Null' can't be assigned to the field 'f', which has type 'int'.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }
}

@reflectiveTest
class ConstEvalThrowsExceptionTest extends PubPackageResolutionTest {
  test_asExpression_typeParameter() async {
    await assertErrorsInCode(
      '''
class C<T> {
  final t;
  const C(dynamic x) : t = x as T;
}

main() {
  const C<int>(0);
  const C<int>('foo');
  const C<int>(null);
}
''',
      [
        error(
          diag.constEvalThrowsException,
          92,
          19,
          contextMessages: [
            contextMessage(
              testFile,
              51,
              6,
              textContains: [
                "The error is in the field initializer of 'C', and occurs here.",
              ],
            ),
          ],
        ),
        error(
          diag.constEvalThrowsException,
          115,
          18,
          contextMessages: [
            contextMessage(
              testFile,
              51,
              6,
              textContains: [
                "The error is in the field initializer of 'C', and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_asExpression_typeParameter_nested() async {
    await assertErrorsInCode(
      '''
class C<T> {
  final t;
  const C(dynamic x) : t = x as List<T>;
}

main() {
  const C<int>(<int>[]);
  const C<int>(<num>[]);
  const C<int>(null);
}
''',
      [
        error(
          diag.constEvalThrowsException,
          104,
          21,
          contextMessages: [
            contextMessage(
              testFile,
              51,
              12,
              textContains: [
                "The error is in the field initializer of 'C', and occurs here.",
              ],
            ),
          ],
        ),
        error(
          diag.constEvalThrowsException,
          129,
          18,
          contextMessages: [
            contextMessage(
              testFile,
              51,
              12,
              textContains: [
                "The error is in the field initializer of 'C', and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_assertInitializer() async {
    await assertErrorsInCode(
      r'''
class A {
  const A(int x, int y) : assert(x < y);
}
var v = const A(3, 2);
''',
      [
        error(
          diag.constEvalThrowsException,
          61,
          13,
          contextMessages: [
            contextMessage(
              testFile,
              36,
              13,
              textContains: [
                "The exception is 'The assertion in this constant expression failed.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_assertInitializer_indirect() async {
    await assertErrorsInCode(
      r'''
class A {
  const A(int i)
  : assert(i == 1); // (2)
}
class B extends A {
  const B(int i) : super(i);
}
main() {
  print(const B(2)); // (1)
}
''',
      [
        error(
          diag.constEvalThrowsException,
          124,
          10,
          contextMessages: [
            contextMessage(
              testFile,
              84,
              1,
              textContains: [
                "The evaluated constructor 'A' is called by 'B' and 'B' is defined here.",
              ],
            ),
            contextMessage(
              testFile,
              31,
              14,
              textContains: [
                "The exception is 'The assertion in this constant expression failed.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_assertInitializer_withMessage() async {
    await assertErrorsInCode(
      r'''
class A {
  const A(int x): assert(x > 0, '$x must be greater than 0');
}
const a = const A(0);
''',
      [
        error(
          diag.constEvalThrowsException,
          84,
          10,
          contextMessages: [
            contextMessage(
              testFile,
              28,
              42,
              textContains: [
                "The exception is 'An assertion failed with message '0 must be greater than 0'.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_assertInitializer_withMessage_cannotCompute() async {
    await assertErrorsInCode(
      r'''
class A {
  const A(int x): assert(x > 0, '${throw ''}');
}
const a = const A(0);
''',
      [
        error(diag.invalidConstant, 45, 8),
        error(diag.constConstructorThrowsException, 45, 8),
        error(diag.deadCode, 54, 3),
        error(
          diag.constEvalThrowsException,
          70,
          10,
          contextMessages: [
            contextMessage(
              testFile,
              28,
              28,
              textContains: [
                "The exception is 'The assertion in this constant expression failed.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_binaryMinus_null() async {
    await assertErrorsInCode(
      '''
const dynamic D = null;
const C = D - 5;
''',
      [error(diag.constEvalThrowsException, 34, 5)],
    );

    await assertErrorsInCode(
      '''
const dynamic D = null;
const C = 5 - D;
''',
      [error(diag.constEvalThrowsException, 34, 5)],
    );
  }

  test_binaryPlus_null() async {
    await assertErrorsInCode(
      '''
const dynamic D = null;
const C = D + 5;
''',
      [error(diag.constEvalThrowsException, 34, 5)],
    );

    await assertErrorsInCode(
      '''
const dynamic D = null;
const C = 5 + D;
''',
      [error(diag.constEvalThrowsException, 34, 5)],
    );
  }

  test_CastError_intToDouble_constructor_importAnalyzedAfter() async {
    // See dartbug.com/35993
    var other = newFile('$testPackageLibPath/other.dart', '''
class Foo {
  final double value;

  const Foo(this.value);
}

class Bar {
  final Foo value;

  const Bar(this.value);

  const Bar.some() : this(const Foo(1));
}''');
    await assertNoErrorsInCode(r'''
import 'other.dart';

void main() {
  const foo = Foo(1);
  const bar = Bar.some();
  print("$foo, $bar");
}
''');
    var otherFileResult = await resolveFile(other);
    expect(otherFileResult.diagnostics, isEmpty);
  }

  test_CastError_intToDouble_constructor_importAnalyzedBefore() async {
    // See dartbug.com/35993
    var other = newFile('$testPackageLibPath/other.dart', '''
class Foo {
  final double value;

  const Foo(this.value);
}

class Bar {
  final Foo value;

  const Bar(this.value);

  const Bar.some() : this(const Foo(1));
}''');
    await assertNoErrorsInCode(r'''
import 'other.dart';

void main() {
  const foo = Foo(1);
  const bar = Bar.some();
  print("$foo, $bar");
}
''');
    var otherFileResult = await resolveFile(other);
    expect(otherFileResult.diagnostics, isEmpty);
  }

  test_default_constructor_arg_empty_map_import() async {
    var other = newFile('$testPackageLibPath/other.dart', '''
class C {
  final Map<String, int> m;
  const C({this.m = const <String, int>{}})
    : assert(m != null);
}
''');
    await assertErrorsInCode(
      '''
import 'other.dart';

main() {
  var c = const C();
}
''',
      [error(diag.unusedLocalVariable, 37, 1)],
    );
    var otherFileResult = await resolveFile(other);
    assertErrorsInList(otherFileResult.diagnostics, [
      error(diag.unnecessaryNullComparisonNeverNullTrue, 97, 7),
    ]);
  }

  test_enum_constructor_initializer_asExpression() async {
    await assertErrorsInCode(
      r'''
enum E {
  v();
  final int x;
  const E({int? x}) : x = x as int;
}
''',
      [
        error(
          diag.constEvalThrowsException,
          11,
          3,
          contextMessages: [
            contextMessage(
              testFile,
              57,
              8,
              textContains: [
                "The error is in the field initializer of 'E', and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_enum_int_null() async {
    await assertErrorsInCode(
      r'''
const dynamic a = null;

enum E {
  v(a);
  const E(int a);
}
''',
      [
        error(
          diag.constEvalThrowsException,
          36,
          4,
          contextMessages: [
            contextMessage(
              testFile,
              38,
              1,
              textContains: [
                "The exception is 'A value of type 'Null' can't be assigned to a parameter of type 'int' in a const constructor.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_enum_int_String() async {
    await assertErrorsInCode(
      r'''
const dynamic a = '0';

enum E {
  v(a);
  const E(int a);
}
''',
      [
        error(
          diag.constEvalThrowsException,
          35,
          4,
          contextMessages: [
            contextMessage(
              testFile,
              37,
              1,
              textContains: [
                "The exception is 'A value of type 'String' can't be assigned to a parameter of type 'int' in a const constructor.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_eqEq_nonPrimitiveRightOperand() async {
    await assertNoErrorsInCode('''
const c = const T.eq(1, const Object());
class T {
  final Object value;
  const T.eq(Object o1, Object o2) : value = o1 == o2;
}
''');
  }

  test_finalAlreadySet_initializer() async {
    // If a final variable has an initializer at the site of its declaration,
    // and at the site of the constructor, then invoking that constructor would
    // produce a runtime error; hence invoking that constructor via the "const"
    // keyword results in a compile-time error.
    await assertErrorsInCode(
      '''
class C {
  final x = 1;
  const C() : x = 2;
}
var x = const C();
''',
      [
        error(diag.fieldInitializedInInitializerAndDeclaration, 39, 1),
        error(diag.constEvalThrowsException, 56, 9),
      ],
    );
  }

  test_finalAlreadySet_initializing_formal() async {
    // If a final variable has an initializer at the site of its declaration,
    // and it is initialized using an initializing formal at the site of the
    // constructor, then invoking that constructor would produce a runtime
    // error; hence invoking that constructor via the "const" keyword results
    // in a compile-time error.
    await assertErrorsInCode(
      '''
class C {
  final x = 1;
  const C(this.x);
}
var x = const C(2);
''',
      [
        error(diag.finalInitializedInDeclarationAndConstructor, 40, 1),
        error(diag.constEvalThrowsException, 54, 10),
      ],
    );
  }

  test_fromEnvironment_assertInitializer() async {
    await assertNoErrorsInCode('''
class A {
  const A(int x) : assert(x >= 0);
}

main() {
  var c = const A(int.fromEnvironment('x'));
  print(c);
}
''');
  }

  test_fromEnvironment_bool_badArgs() async {
    await assertErrorsInCode(
      r'''
var b1 = const bool.fromEnvironment(1);
var b2 = const bool.fromEnvironment('x', defaultValue: 1);
''',
      [
        error(diag.constEvalThrowsException, 9, 29),
        error(diag.argumentTypeNotAssignable, 36, 1),
        error(diag.constEvalThrowsException, 49, 48),
        error(diag.argumentTypeNotAssignable, 95, 1),
      ],
    );
  }

  test_fromEnvironment_bool_badDefault_whenDefined() async {
    // The type of the defaultValue needs to be correct even when the default
    // value isn't used (because the variable is defined in the environment).
    declaredVariables = {'x': 'true'};
    await assertErrorsInCode(
      '''
var b = const bool.fromEnvironment('x', defaultValue: 1);
''',
      [
        error(diag.constEvalThrowsException, 8, 48),
        error(diag.argumentTypeNotAssignable, 54, 1),
      ],
    );
  }

  test_fromEnvironment_ifElement() async {
    await assertNoErrorsInCode('''
const b = bool.fromEnvironment('foo');

main() {
  const l1 = [1, 2, 3];
  const l2 = [if (b) ...l1];
  print(l2);
}
''');
  }

  test_ifElement_false_thenNotEvaluated() async {
    await assertNoErrorsInCode('''
const dynamic nil = null;
const c = [if (1 < 0) nil + 1];
''');
  }

  test_ifElement_true_elseNotEvaluated() async {
    await assertNoErrorsInCode('''
const dynamic nil = null;
const c = [if (0 < 1) 3 else nil + 1];
''');
  }

  test_redirectingConstructor_paramTypeMismatch() async {
    await assertErrorsInCode(
      r'''
class A {
  const A.a1(x) : this.a2(x);
  const A.a2(String x);
}
var v = const A.a1(0);
''',
      [
        error(
          diag.constEvalThrowsException,
          74,
          13,
          contextMessages: [
            contextMessage(
              testFile,
              36,
              1,
              textContains: [
                "The exception is 'A value of type 'int' can't be assigned to a parameter of type 'String' in a const constructor.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_superConstructor_paramTypeMismatch() async {
    await assertErrorsInCode(
      r'''
class C {
  final double d;
  const C(this.d);
}
class D extends C {
  const D(d) : super(d);
}
const f = const D('0.0');
''',
      [
        error(
          diag.constEvalThrowsException,
          106,
          14,
          contextMessages: [
            contextMessage(
              testFile,
              77,
              1,
              textContains: [
                "The evaluated constructor 'C' is called by 'D' and 'D' is defined here.",
              ],
            ),
            contextMessage(
              testFile,
              90,
              1,
              textContains: [
                "The exception is 'A value of type 'String' can't be assigned to a parameter of type 'double' in a const constructor.' and occurs here.",
              ],
            ),
          ],
        ),
      ],
    );
  }

  test_symbolConstructor_nonStringArgument() async {
    await assertErrorsInCode(
      r'''
var s2 = const Symbol(3);
''',
      [
        error(diag.constEvalThrowsException, 9, 15),
        error(diag.argumentTypeNotAssignable, 22, 1),
      ],
    );
  }

  test_symbolConstructor_string_digit() async {
    await assertNoErrorsInCode(r'''
var s = const Symbol('3');
''');
  }

  test_symbolConstructor_string_underscore() async {
    await assertNoErrorsInCode(r'''
var s = const Symbol('_');
''');
  }

  test_unaryBitNot_null() async {
    await assertErrorsInCode(
      '''
const dynamic D = null;
const C = ~D;
''',
      [error(diag.constEvalThrowsException, 34, 2)],
    );
  }

  test_unaryNegated_null() async {
    await assertErrorsInCode(
      '''
const dynamic D = null;
const C = -D;
''',
      [error(diag.constEvalThrowsException, 34, 2)],
    );
  }

  test_unaryNot_null() async {
    await assertErrorsInCode(
      '''
const dynamic D = null;
const C = !D;
''',
      [error(diag.constEvalThrowsException, 34, 2)],
    );
  }
}
