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

import 'dartdoc_test_base.dart';
import 'src/utils.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(ClassesTest);
  });
}

@reflectiveTest
class ClassesTest extends DartdocTestBase {
  @override
  String get libraryName => 'classes';

  void test_availableExtensions_onClass() async {
    var library = await bootPackageWithLibrary('''
class C {}
extension Ex on C {}
''');

    var c = library.classes.named('C');
    expect(
      c.potentiallyApplicableExtensionsSorted,
      contains(library.extensions.named('Ex')),
    );
  }

  void test_availableExtensions_onClassWithInstantiatedType() async {
    var library = await bootPackageWithLibrary('''
class C<T> {}
extension Ex on C<int> {}
''');

    var c = library.classes.named('C');
    expect(
      c.potentiallyApplicableExtensionsSorted,
      contains(library.extensions.named('Ex')),
    );
  }

  void test_availableExtensions_onSubtypeOfClass() async {
    var library = await bootPackageWithLibrary('''
class C {}
class D extends C {}
extension Ex on C {}
''');

    var d = library.classes.named('D');
    expect(
      d.potentiallyApplicableExtensionsSorted,
      contains(library.extensions.named('Ex')),
    );
  }

  void test_availableExtensions_onInstantiatedSubtypeOfClass() async {
    var library = await bootPackageWithLibrary('''
class C<T> {}
class D<T> extends C<T> {}
extension Ex on C<int> {}
''');

    var d = library.classes.named('D');
    expect(
      d.potentiallyApplicableExtensionsSorted,
      contains(library.extensions.named('Ex')),
    );
  }

  void test_publicInterfaces_direct() async {
    var library = await bootPackageWithLibrary('''
class A {}
class B implements A {}
''');

    var b = library.classes.named('B');
    expect(b.publicInterfaces, hasLength(1));
    expect(b.publicInterfaces.first.modelElement, library.classes.named('A'));
  }

  void test_publicInterfaces_indirect() async {
    var library = await bootPackageWithLibrary('''
class A {}
class B implements A {}
class C implements B {}
''');

    var c = library.classes.named('C');
    // Only `B` is shown, not indirect-through-public like `A`.
    expect(c.publicInterfaces, hasLength(1));
    expect(c.publicInterfaces.first.modelElement, library.classes.named('B'));
  }

  void test_publicInterfaces_indirectViaPrivate() async {
    var library = await bootPackageWithLibrary('''
class A {}
class _B implements A {}
class C implements _B {}
''');

    var c = library.classes.named('C');
    expect(c.publicInterfaces, hasLength(1));
    expect(c.publicInterfaces.first.modelElement, library.classes.named('A'));
  }

  void test_publicInterfaces_indirectViaPrivate2() async {
    var library = await bootPackageWithLibrary('''
class A {}
class _B implements A {}
class _C implements _B {}
class D implements _C {}
''');

    var d = library.classes.named('D');
    expect(d.publicInterfaces, hasLength(1));
    expect(d.publicInterfaces.first.modelElement, library.classes.named('A'));
  }

  void test_publicInterfaces_indirectViaPrivateExtendedClass() async {
    var library = await bootPackageWithLibrary('''
class A {}
class _B implements A {}
class C extends _B {}
''');

    var c = library.classes.named('C');
    expect(c.publicInterfaces, hasLength(1));
    expect(c.publicInterfaces.first.modelElement, library.classes.named('A'));
  }

  void test_publicInterfaces_indirectViaPrivateExtendedClass2() async {
    var library = await bootPackageWithLibrary('''
class A {}
class _B implements A {}
class _C extends _B {}
class D extends _C {}
''');

    var c = library.classes.named('D');
    expect(c.publicInterfaces, hasLength(1));
    expect(c.publicInterfaces.first.modelElement, library.classes.named('A'));
  }

  void test_publicInterfaces_onlyExtendedClasses() async {
    var library = await bootPackageWithLibrary('''
class A {}
class B extends A {}
class _C extends B {}
class D extends _C {}
''');

    expect(library.classes.named('D').publicInterfaces, isEmpty);
  }

  void test_publicInterfaces_indirectViaPrivateMixedInMixin() async {
    var library = await bootPackageWithLibrary('''
class A {}
mixin _M implements A {}
class C with _M {}
''');

    var c = library.classes.named('C');
    expect(c.publicInterfaces, hasLength(1));
    expect(c.publicInterfaces.first.modelElement, library.classes.named('A'));
  }

  void test_publicInterfaces_indirectViaPrivate_multiply() async {
    var library = await bootPackageWithLibrary('''
class A<T> {}
class _B<U> implements A<U> {}
class C<T> implements A<T>, _B<T> {}
''');

    var c = library.classes.named('C');
    expect(c.publicInterfaces, hasLength(1));
    expect(c.publicInterfaces.first.modelElement, library.classes.named('A'));
  }

  void test_inheritanceOfObjectInstanceMethod() async {
    // This code is written such that `Inheritable._inheritance` for
    // `A.toString()` includes two copies of Object; one from walking up B's
    // chain, and then the second from walking up C's chain.
    var library = await bootPackageWithLibrary('''
class A implements B, C {}
class B implements C {}
class C implements D {}
class D implements Object {}
''');

    var toString = library.classes.named('A').instanceMethods.named('toString');
    expect(toString.canonicalEnclosingContainer!.isDartCoreObject, isTrue);
  }

  void test_multiplyInheritedOperator_oneIsPrivate() async {
    // Test an edge case where inherited ExecutableElements can come both from
    // private classes and public interfaces.  The test makes sure the class
    // still takes precedence.
    // See https://github.com/dart-lang/dartdoc/issues/1561.
    var library = await bootPackageWithLibrary('''
abstract class A<K, V> {
  void operator []=(K key, V value);
}

abstract class B<K, V> implements A<K, V> {
  @override
  operator []=(K key, V value);
}

abstract class C<K, V> extends B<K, V> {}

abstract class _D<K, V> implements A<K, V> {
  @override
  void operator []=(K key, V value);
}

abstract class E<K, V> = C<K, V> with _D<K, V>;
''');

    var indexAssign =
        library.classes.named('E').inheritedOperators.named('operator []=');
    expect(indexAssign.element.enclosingElement!.name, '_D');
    expect(indexAssign.canonicalEnclosingContainer!.name, 'E');
    expect(indexAssign.canonicalModelElement!.enclosingElement!.name, 'E');
  }

  // TODO(srawlins): Test everything else about classes.
}
