// Copyright (c) 2020, 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/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/dart/element/class_hierarchy.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../../../generated/elements_types_mixin.dart';
import '../../../generated/type_system_base.dart';

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

@reflectiveTest
class ClassHierarchyTest extends AbstractTypeSystemTest
    with _AbstractClassHierarchyMixin {
  @override
  void setUp() {
    super.setUp();
    _createSharedElements();
  }

  test_invalid() {
    _checkA(
      typeArguments: [intNone, doubleNone],
      interfaces: ['A<int>'],
      errors: ['A<int> vs. A<double>'],
    );
  }

  test_valid_equal() {
    _checkA(typeArguments: [intNone, intNone], interfaces: ['A<int>']);
  }

  test_valid_equal_neverNone() {
    _checkA(typeArguments: [neverNone, neverNone], interfaces: ['A<Never>']);
  }

  test_valid_merge() {
    _checkA(
      typeArguments: [objectQuestion, dynamicType],
      interfaces: ['A<Object?>'],
    );
  }
}

mixin _AbstractClassHierarchyMixin on ElementsTypesMixin {
  late ClassElementImpl A;

  void _assertErrors(List<ClassHierarchyError> errors, List<String> expected) {
    expect(
      errors.map((e) {
        if (e is IncompatibleInterfacesClassHierarchyError) {
          var firstStr = _interfaceString(e.first);
          var secondStr = _interfaceString(e.second);
          return '$firstStr vs. $secondStr';
        } else {
          throw UnimplementedError('${e.runtimeType}');
        }
      }).toList(),
      unorderedEquals(expected),
    );
  }

  void _assertInterfaces(
    List<InterfaceType> interfaces,
    List<String> expected,
  ) {
    var interfacesStr = interfaces.map(_interfaceString).toList();
    expect(interfacesStr, unorderedEquals(['Object', ...expected]));
  }

  void _checkA({
    required List<TypeImpl> typeArguments,
    required List<String> interfaces,
    List<String> errors = const [],
  }) {
    var specifiedInterfaces = typeArguments
        .map((e) => interfaceTypeNone(A, typeArguments: [e]))
        .toList();
    var X = class_2(name: 'X', interfaces: specifiedInterfaces);

    var classHierarchy = ClassHierarchy();

    var actualInterfaces = classHierarchy.implementedInterfaces(X);
    _assertInterfaces(actualInterfaces, interfaces);

    var actualErrors = classHierarchy.errors(X);
    _assertErrors(actualErrors, errors);
  }

  void _createSharedElements() {
    var T = typeParameter('T');
    A = class_2(name: 'A', typeParameters: [T]);
  }

  String _interfaceString(InterfaceType interface) {
    return (interface as InterfaceTypeImpl)
        .withNullability(NullabilitySuffix.none)
        .getDisplayString();
  }
}
