// Copyright (c) 2023, 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/error/codes.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

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

main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(NonExhaustiveSwitchExpressionTest);
    defineReflectiveTests(NonExhaustiveSwitchStatementTest);
  });
}

@reflectiveTest
class NonExhaustiveSwitchExpressionTest extends PubPackageResolutionTest {
  test_bool_true() async {
    await assertErrorsInCode(
      r'''
Object f(bool x) {
  return switch (x) {
    true => 0,
  };
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchExpression, 28, 6)],
    );
  }

  test_bool_true_false() async {
    await assertNoErrorsInCode(r'''
Object f(bool x) {
  return switch (x) {
    true => 1,
    false => 0,
  };
}
''');
  }

  test_class_int_wildcard() async {
    await assertNoErrorsInCode(r'''
Object f(int x) {
  return switch (x) {
    0 => 0,
    _ => 1,
  };
}
''');
  }

  test_class_withField_wildcard() async {
    await assertNoErrorsInCode(r'''
Object f(int x) {
  return switch (x) {
    int(isEven: true) => 0,
    _ => 1,
  };
}
''');
  }

  test_enum_2at2_hasWhen() async {
    await assertErrorsInCode(
      r'''
enum E {
  a, b
}

Object f(E x) {
  return switch (x) {
    E.a when 1 == 0 => 0,
    E.b => 1,
  };
}
''',
      [
        error(
          CompileTimeErrorCode.nonExhaustiveSwitchExpression,
          44,
          6,
          correctionContains: 'E.a',
        ),
      ],
    );
  }

  test_invalidType_empty() async {
    await assertErrorsInCode(
      r'''
void f(Unresolved x) => switch (x) {};
''',
      [error(CompileTimeErrorCode.undefinedClass, 7, 10)],
    );
  }
}

@reflectiveTest
class NonExhaustiveSwitchStatementTest extends PubPackageResolutionTest {
  test_alwaysExhaustive_bool_true() async {
    await assertErrorsInCode(
      r'''
void f(bool x) {
  switch (x) {
    case true:
      break;
  }
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 19, 6)],
    );
  }

  test_alwaysExhaustive_bool_true_false() async {
    await assertNoErrorsInCode(r'''
void f(bool x) {
  switch (x) {
    case true:
    case false:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_bool_wildcard_typed_bool() async {
    await assertNoErrorsInCode(r'''
void f(bool x) {
  switch (x) {
    case bool _:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_bool_wildcard_typed_int() async {
    await assertErrorsInCode(
      r'''
void f(bool x) {
  switch (x) {
    case int _:
      break;
  }
}
''',
      [
        error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 19, 6),
        error(WarningCode.patternNeverMatchesValueType, 41, 3),
      ],
    );
  }

  test_alwaysExhaustive_bool_wildcard_untyped() async {
    await assertNoErrorsInCode(r'''
void f(bool x) {
  switch (x) {
    case _:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_boolNullable_true_false() async {
    await assertErrorsInCode(
      r'''
void f(bool? x) {
  switch (x) {
    case true:
    case false:
      break;
  }
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 20, 6)],
    );
  }

  test_alwaysExhaustive_boolNullable_true_false_null() async {
    await assertNoErrorsInCode(r'''
void f(bool? x) {
  switch (x) {
    case true:
    case false:
    case null:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_enum_2at1() async {
    await assertErrorsInCode(
      r'''
enum E {
  a, b
}

void f(E x) {
  switch (x) {
    case E.a:
      break;
  }
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 35, 6)],
    );
  }

  test_alwaysExhaustive_enum_2at2_cases() async {
    await assertNoErrorsInCode(r'''
enum E {
  a, b
}

void f(E x) {
  switch (x) {
    case E.a:
    case E.b:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_enum_2at2_hasWhen() async {
    await assertErrorsInCode(
      r'''
enum E {
  a, b
}

void f(E x) {
  switch (x) {
    case E.a when 1 == 0:
    case E.b:
      break;
  }
}
''',
      [
        error(
          CompileTimeErrorCode.nonExhaustiveSwitchStatement,
          35,
          6,
          correctionContains: 'E.a',
        ),
      ],
    );
  }

  test_alwaysExhaustive_enum_2at2_logicalOr() async {
    await assertNoErrorsInCode(r'''
enum E {
  a, b
}

void f(E x) {
  switch (x) {
    case E.a || E.b:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_enum_cannotCompute() async {
    await assertErrorsInCode(
      r'''
enum E {
  v1(v2), v2(v1);
  const E(Object f);
}

void f(E x) {
  switch (x) {
    case E.v1:
    case E.v2:
      break;
  }
}
''',
      [
        error(CompileTimeErrorCode.recursiveCompileTimeConstant, 11, 2),
        error(CompileTimeErrorCode.recursiveCompileTimeConstant, 19, 2),
      ],
    );
  }

  test_alwaysExhaustive_Null_hasError() async {
    await assertErrorsInCode(
      r'''
void f(Null x) {
  switch (x) {}
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 19, 6)],
    );
  }

  test_alwaysExhaustive_Null_noError() async {
    await assertNoErrorsInCode(r'''
void f(Null x) {
  switch (x) {
    case null:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_recordType_bool_bool_4at4() async {
    await assertNoErrorsInCode(r'''
void f((bool, bool) x) {
  switch (x) {
    case (false, false):
    case (false, true):
    case (true, false):
    case (true, true):
      break;
  }
}
''');
  }

  test_alwaysExhaustive_sealedClass_2at1() async {
    await assertErrorsInCode(
      r'''
sealed class A {}
class B extends A {}
class C extends A {}

void f(A x) {
  switch (x) {
    case B():
      break;
  }
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 77, 6)],
    );
  }

  test_alwaysExhaustive_sealedClass_2at2() async {
    await assertNoErrorsInCode(r'''
sealed class A {}
class B extends A {}
class C extends A {}

void f(A x) {
  switch (x) {
    case B():
      break;
    case C():
      break;
  }
}
''');
  }

  test_alwaysExhaustive_sealedClass_2at2_wildcard() async {
    await assertNoErrorsInCode(r'''
sealed class A {}
class B extends A {}
class C extends A {}

void f(A x) {
  switch (x) {
    case B():
      break;
    case _:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_sealedClass_constraintsMixin() async {
    await assertErrorsInCode(
      r'''
sealed class A {}

class B extends A {}

mixin M on A {}

void f(A x) {
  switch (x) {
    case B _:
      break;
  }
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 74, 6)],
    );
  }

  test_alwaysExhaustive_sealedClass_hasExtensionType_1of1() async {
    await assertNoErrorsInCode(r'''
sealed class A {}
class B extends A {}
extension type EA(A it) implements A {}

void f(A x) {
  switch (x) {
    case B():
      break;
  }
}
''');
  }

  test_alwaysExhaustive_sealedClass_hasExtensionType_1of2() async {
    await assertErrorsInCode(
      r'''
sealed class A {}
class B extends A {}
class C extends A {}
extension type EA(A it) implements A {}

void f(A x) {
  switch (x) {
    case B():
      break;
  }
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 117, 6)],
    );
  }

  test_alwaysExhaustive_sealedClass_implementedByEnum_3at2() async {
    await assertErrorsInCode(
      r'''
sealed class A {}

class B implements A {}

enum E implements A {
  a, b
}

void f(A x) {
  switch (x) {
    case B _:
    case E.a:
      break;
  }
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 92, 6)],
    );
  }

  test_alwaysExhaustive_sealedClass_implementedByEnum_3at3() async {
    await assertNoErrorsInCode(r'''
sealed class A {}

class B implements A {}

enum E implements A {
  a, b
}

void f(A x) {
  switch (x) {
    case B _:
    case E.a:
    case E.b:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_sealedClass_implementedByMixin_2at1() async {
    await assertErrorsInCode(
      r'''
sealed class A {}

class B implements A {}

mixin M implements A {}

void f(A x) {
  switch (x) {
    case B _:
      break;
  }
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 85, 6)],
    );
  }

  test_alwaysExhaustive_sealedClass_implementedByMixin_2at2() async {
    await assertNoErrorsInCode(r'''
sealed class A {}

class B implements A {}

mixin M implements A {}

void f(A x) {
  switch (x) {
    case B _:
    case M _:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_sealedClass_unresolvedIdentifier() async {
    await assertErrorsInCode(
      r'''
sealed class A {}
class B extends A {}

void f(A x) {
  switch (x) {
    case unresolved:
      break;
  }
}
''',
      [error(CompileTimeErrorCode.undefinedIdentifier, 78, 10)],
    );
  }

  test_alwaysExhaustive_sealedClass_unresolvedObject() async {
    await assertErrorsInCode(
      r'''
sealed class A {}
class B extends A {}

void f(A x) {
  switch (x) {
    case Unresolved():
      break;
  }
}
''',
      [error(CompileTimeErrorCode.undefinedClass, 78, 10)],
    );
  }

  test_alwaysExhaustive_typeVariable_bound_bool_true() async {
    await assertErrorsInCode(
      r'''
void f<T extends bool>(T x) {
  switch (x) {
    case true:
      break;
  }
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 32, 6)],
    );
  }

  test_alwaysExhaustive_typeVariable_bound_bool_true_false() async {
    await assertNoErrorsInCode(r'''
void f<T extends bool>(T x) {
  switch (x) {
    case true:
    case false:
      break;
  }
}
''');
  }

  test_alwaysExhaustive_typeVariable_promoted_bool_true() async {
    await assertErrorsInCode(
      r'''
void f<T>(T x) {
  if (x is bool) {
    switch (x) {
      case true:
        break;
    }
  }
}
''',
      [error(CompileTimeErrorCode.nonExhaustiveSwitchStatement, 40, 6)],
    );
  }

  test_alwaysExhaustive_typeVariable_promoted_bool_true_false() async {
    await assertNoErrorsInCode(r'''
void f<T>(T x) {
  if (x is bool) {
    switch (x) {
      case true:
      case false:
        break;
    }
  }
}
''');
  }

  test_invalidType_empty() async {
    await assertErrorsInCode(
      r'''
void f(Unresolved x) {
  switch (x) {}
}
''',
      [error(CompileTimeErrorCode.undefinedClass, 7, 10)],
    );
  }

  test_notAlwaysExhaustive_int() async {
    await assertNoErrorsInCode(r'''
void f(int x) {
  switch (x) {
    case 0:
      break;
  }
}
''');
  }
}
