// 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_reflective_loader/test_reflective_loader.dart';

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

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

@reflectiveTest
class InvalidUseOfProtectedMemberTest extends PubPackageResolutionTest {
  @override
  void setUp() {
    super.setUp();
    writeTestPackageConfigWithMeta();
  }

  test_closure() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';

class A {
  @protected
  int a() => 42;
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';

void main() {
  var leak = new A().a;
  print(leak);
}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(
        diag.invalidUseOfProtectedMember,
        56,
        1,
        text:
            "The member 'a' can only be used within instance members of subclasses of 'A'.",
      ),
    ]);
  }

  test_extendingSubclass() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected
  void a(){ }
}
class B extends A {
  void b() => a();
}''');
  }

  test_extension_outsideClassAndFile() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
class A {
  @protected
  void a(int i) {}
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';
extension E on A {
  e() {
    a(7);
  }
}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(diag.invalidUseOfProtectedMember, 51, 1),
    ]);
  }

  test_extensionType_implementedMember() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class C {
  @protected
  void f(){}
}
extension type E(C c) implements C { }

void main() {
  E(C()).f();
}
''');
  }

  test_extensionType_implementedMember_outsideClassAndLibrary() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
class C {
  @protected
  void f(){}
}
extension type E(C c) implements C { }
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';
void main() {
  E(C()).f();
}
''');
    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(diag.invalidUseOfProtectedMember, 43, 1),
    ]);
  }

  test_extensionType_member() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
extension type E(int i) {
  @protected
  void f(){}
}
void main() {
  E(1).f();
}
''');
  }

  test_extensionType_member_outsideClassAndLibrary() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
extension type E(int i) {
  @protected
  void f(){}
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';
void main() {
  E(1).f();
}
''');
    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(diag.invalidUseOfProtectedMember, 41, 1),
    ]);
  }

  test_field() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected
  int a = 42;
}
class B extends A {
  int b() => a;
}
''');
  }

  test_field_outsideClassAndLibrary() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
class A {
  @protected
  int f = 0;
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';
abstract class B {
  int m(A a) => a.f;
}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(diag.invalidUseOfProtectedMember, 57, 1),
    ]);
  }

  test_field_outsideClassAndLibrary_originPrimaryConstructor() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
class A(@protected var int f);
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';
abstract class B {
  int m(A a) => a.f;
}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(diag.invalidUseOfProtectedMember, 57, 1),
    ]);
  }

  test_field_subclassAndSameLibrary() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected
  int a = 0;
}
abstract class B implements A {
  int b() => a;
}''');
  }

  test_fromSuperclassConstraint() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
abstract class A {
  @protected
  void foo() {}
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';
mixin M on A {
  @override
  void foo() {
    super.foo();
  }
}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, []);
  }

  test_function_outsideClassAndLibrary() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
class A {
  @protected
  void a(){ }
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';

main() {
  new A().a();
}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(diag.invalidUseOfProtectedMember, 40, 1),
    ]);
  }

  test_function_sameLibrary() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected
  void a(){ }
}
main() {
  new A().a();
}''');
  }

  test_function_subclass() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected
  int a() => 0;
}

abstract class B implements A {
  int b() => a();
}''');
  }

  test_getter() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected
  int get a => 42;
}
class B extends A {
  int b() => a;
}
''');
  }

  test_getter_outsideClassAndLibrary() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
class A {
  @protected
  int get a => 42;
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';
class B {
  A a = A();
  int b() => a.a;
}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(diag.invalidUseOfProtectedMember, 58, 1),
    ]);
  }

  test_getter_outsideClassAndLibrary_inObjectPattern() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
class A {
  @protected
  int get a => 42;
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';
void f(Object o) {
  switch (o) {
    case A(a: 7): print('yes');
  }
}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(diag.invalidUseOfProtectedMember, 65, 1),
    ]);
  }

  test_getter_subclass() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected
  int get a => 42;
}
abstract class B implements A {
  int b() => a;
}''');
  }

  test_inDocs() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';

class A {
  @protected
  int c = 0;

  @protected
  int get b => 0;

  @protected
  int a() => 0;
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';
/// OK: [A.a], [A.b], [A.c].
f() {}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, []);
  }

  test_method_outsideClassAndLibrary() async {
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
class A {
  @protected
  void a() {}
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';

class B {
  void b() => new A().a();
}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(diag.invalidUseOfProtectedMember, 53, 1),
    ]);
  }

  test_method_subclass() async {
    // https://github.com/dart-lang/linter/issues/257
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';

typedef void VoidCallback();

class State<E> {
  @protected
  void setState(VoidCallback fn) {}
}

class Button extends State<Object> {
  void handleSomething() {
    setState(() {});
  }
}
''');
  }

  test_mixingIn() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
mixin A {
  @protected
  void a(){ }
}
class B extends Object with A {
  void b() => a();
}''');
  }

  test_mixingIn_asParameter() async {
    // TODO(srawlins): This test verifies that the analyzer **allows**
    // protected members to be called from static members, which violates the
    // protected spec.
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected m1() {}
}
class B extends A {
  static m2(A a) => a.m1();
}''');
  }

  test_sameLibrary() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected
  void a(){ }
}
class B extends A {
  void a() => a();
}
main() {
  new B().a();
}''');
  }

  test_setter_outsideClassAndFile() async {
    // TODO(srawlins): This test verifies that the analyzer **allows**
    // protected members to be called on objects other than `this`, which
    // violates the protected spec.
    var lib1 = newFile('$testPackageLibPath/lib1.dart', r'''
import 'package:meta/meta.dart';
class A {
  @protected
  void set a(int i) { }
}
''');
    var lib2 = newFile('$testPackageLibPath/lib2.dart', r'''
import 'lib1.dart';
class B {
  A a = A();
  b(int i) {
    a.a = i;
  }
}
''');

    await assertErrorsInFile2(lib1, []);
    await assertErrorsInFile2(lib2, [
      error(diag.invalidUseOfProtectedMember, 62, 1),
    ]);
  }

  test_setter_sameClass() async {
    await assertErrorsInCode(
      r'''
import 'package:meta/meta.dart';
class A {
  int _a = 0;
  @protected
  void set a(int a) { _a = a; }
  A(int a) {
    this.a = a;
  }
}
''',
      [error(diag.unusedField, 49, 2)],
    );
  }

  test_setter_subclass() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected
  void set a(int i) { }
}
class B extends A {
  void b(int i) {
    a = i;
  }
}
''');
  }

  test_setter_subclassImplementing() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
class A {
  @protected
  void set a(int i) { }
}
abstract class B implements A {
  b(int i) {
    a = i;
  }
}''');
  }

  test_topLevelVariable() async {
    await assertNoErrorsInCode(r'''
import 'package:meta/meta.dart';
@protected
int x = 0;
main() {
  print(x);
}''');
    // TODO(brianwilkerson): This should produce a hint because the
    // annotation is being applied to the wrong kind of declaration.
  }
}
