/*
 * Copyright (C) 2015-2016 Federico Tomassetti
 * Copyright (C) 2017-2024 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */

package com.github.javaparser.symbolsolver.model.typesystem;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.resolution.TypeSolver;
import com.github.javaparser.resolution.declarations.*;
import com.github.javaparser.resolution.declarations.ResolvedTypeParameterDeclaration.Bound;
import com.github.javaparser.resolution.model.typesystem.NullType;
import com.github.javaparser.resolution.model.typesystem.ReferenceTypeImpl;
import com.github.javaparser.resolution.types.*;
import com.github.javaparser.symbolsolver.AbstractSymbolResolutionTest;
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
import com.github.javaparser.symbolsolver.reflectionmodel.ReflectionClassDeclaration;
import com.github.javaparser.symbolsolver.reflectionmodel.ReflectionInterfaceDeclaration;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.io.Serializable;
import java.net.ProtocolException;
import java.nio.Buffer;
import java.nio.CharBuffer;
import java.nio.file.FileSystemException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class ReferenceTypeTest extends AbstractSymbolResolutionTest {

    private ReferenceTypeImpl listOfA;
    private ReferenceTypeImpl listOfStrings;
    private ReferenceTypeImpl linkedListOfString;
    private ReferenceTypeImpl collectionOfString;
    private ReferenceTypeImpl listOfWildcardExtendsString;
    private ReferenceTypeImpl listOfWildcardSuperString;
    private ReferenceTypeImpl object;
    private ReferenceTypeImpl string;
    private TypeSolver typeSolver;
    private ReferenceTypeImpl ioException;
    private ResolvedType unionWithIOExceptionAsCommonAncestor;
    private ResolvedType unionWithThrowableAsCommonAncestor;

    @BeforeEach
    void setup() {
        typeSolver = new ReflectionTypeSolver();
        object = new ReferenceTypeImpl(new ReflectionClassDeclaration(Object.class, typeSolver));
        string = new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeSolver));
        listOfA = new ReferenceTypeImpl(
                new ReflectionInterfaceDeclaration(List.class, typeSolver),
                ImmutableList.of(new ResolvedTypeVariable(
                        ResolvedTypeParameterDeclaration.onType("A", "foo.Bar", Collections.emptyList()))));
        listOfStrings = new ReferenceTypeImpl(
                new ReflectionInterfaceDeclaration(List.class, typeSolver),
                ImmutableList.of(new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeSolver))));
        linkedListOfString = new ReferenceTypeImpl(
                new ReflectionClassDeclaration(LinkedList.class, typeSolver),
                ImmutableList.of(new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeSolver))));
        collectionOfString = new ReferenceTypeImpl(
                new ReflectionInterfaceDeclaration(Collection.class, typeSolver),
                ImmutableList.of(new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeSolver))));
        listOfWildcardExtendsString = new ReferenceTypeImpl(
                new ReflectionInterfaceDeclaration(List.class, typeSolver),
                ImmutableList.of(ResolvedWildcard.extendsBound(string)));
        listOfWildcardSuperString = new ReferenceTypeImpl(
                new ReflectionInterfaceDeclaration(List.class, typeSolver),
                ImmutableList.of(ResolvedWildcard.superBound(string)));
        ioException = new ReferenceTypeImpl(new ReflectionClassDeclaration(IOException.class, typeSolver));
        unionWithIOExceptionAsCommonAncestor = new ResolvedUnionType(Arrays.asList(
                new ReferenceTypeImpl(new ReflectionClassDeclaration(ProtocolException.class, typeSolver)),
                new ReferenceTypeImpl(new ReflectionClassDeclaration(FileSystemException.class, typeSolver))));
        unionWithThrowableAsCommonAncestor = new ResolvedUnionType(Arrays.asList(
                new ReferenceTypeImpl(new ReflectionClassDeclaration(ClassCastException.class, typeSolver)),
                new ReferenceTypeImpl(new ReflectionClassDeclaration(AssertionError.class, typeSolver))));

        // minimal initialization of JavaParser
        ParserConfiguration configuration =
                new ParserConfiguration().setSymbolResolver(new JavaSymbolSolver(new ReflectionTypeSolver()));
        // Setup parser
        StaticJavaParser.setConfiguration(configuration);
    }

    @Test
    void testDerivationOfTypeParameters() {
        ReflectionTypeSolver typeSolver = new ReflectionTypeSolver();
        ReferenceTypeImpl ref1 = new ReferenceTypeImpl(typeSolver.solveType(LinkedList.class.getCanonicalName()));
        assertEquals(1, ref1.typeParametersValues().size());
        assertEquals(true, ref1.typeParametersValues().get(0).isTypeVariable());
        assertEquals("E", ref1.typeParametersValues().get(0).asTypeParameter().getName());
    }

    @Test
    void testIsArray() {
        assertEquals(false, object.isArray());
        assertEquals(false, string.isArray());
        assertEquals(false, listOfA.isArray());
        assertEquals(false, listOfStrings.isArray());
    }

    @Test
    void testIsPrimitive() {
        assertEquals(false, object.isPrimitive());
        assertEquals(false, string.isPrimitive());
        assertEquals(false, listOfA.isPrimitive());
        assertEquals(false, listOfStrings.isPrimitive());
    }

    @Test
    void testIsNull() {
        assertEquals(false, object.isNull());
        assertEquals(false, string.isNull());
        assertEquals(false, listOfA.isNull());
        assertEquals(false, listOfStrings.isNull());
    }

    @Test
    void testIsReference() {
        assertEquals(true, object.isReference());
        assertEquals(true, string.isReference());
        assertEquals(true, listOfA.isReference());
        assertEquals(true, listOfStrings.isReference());
    }

    @Test
    void testIsReferenceType() {
        assertEquals(true, object.isReferenceType());
        assertEquals(true, string.isReferenceType());
        assertEquals(true, listOfA.isReferenceType());
        assertEquals(true, listOfStrings.isReferenceType());
    }

    @Test
    void testIsVoid() {
        assertEquals(false, object.isVoid());
        assertEquals(false, string.isVoid());
        assertEquals(false, listOfA.isVoid());
        assertEquals(false, listOfStrings.isVoid());
    }

    @Test
    void testIsTypeVariable() {
        assertEquals(false, object.isTypeVariable());
        assertEquals(false, string.isTypeVariable());
        assertEquals(false, listOfA.isTypeVariable());
        assertEquals(false, listOfStrings.isTypeVariable());
    }

    @Test
    void testAsReferenceTypeUsage() {
        assertTrue(object == object.asReferenceType());
        assertTrue(string == string.asReferenceType());
        assertTrue(listOfA == listOfA.asReferenceType());
        assertTrue(listOfStrings == listOfStrings.asReferenceType());
    }

    @Test
    void testAsTypeParameter() {
        assertThrows(UnsupportedOperationException.class, () -> object.asTypeParameter());
    }

    @Test
    void testAsArrayTypeUsage() {
        assertThrows(UnsupportedOperationException.class, () -> object.asArrayType());
    }

    @Test
    void testAsDescribe() {
        assertEquals("java.lang.Object", object.describe());
        assertEquals("java.lang.String", string.describe());
        assertEquals("java.util.List<A>", listOfA.describe());
        assertEquals("java.util.List<java.lang.String>", listOfStrings.describe());
    }

    @Test
    void testReplaceParam() {
        ResolvedTypeParameterDeclaration tpA =
                ResolvedTypeParameterDeclaration.onType("A", "foo.Bar", Collections.emptyList());
        assertTrue(object == object.replaceTypeVariables(tpA, object));
        assertTrue(string == string.replaceTypeVariables(tpA, object));
        assertEquals(listOfStrings, listOfStrings.replaceTypeVariables(tpA, object));
        assertEquals(listOfStrings, listOfA.replaceTypeVariables(tpA, string));
    }

    @Test
    void testIsAssignableBySimple() {
        assertEquals(true, object.isAssignableBy(string));
        assertEquals(false, string.isAssignableBy(object));
        assertEquals(false, listOfStrings.isAssignableBy(listOfA));
        assertEquals(false, listOfA.isAssignableBy(listOfStrings));

        assertEquals(false, object.isAssignableBy(ResolvedVoidType.INSTANCE));
        assertEquals(false, string.isAssignableBy(ResolvedVoidType.INSTANCE));
        assertEquals(false, listOfStrings.isAssignableBy(ResolvedVoidType.INSTANCE));
        assertEquals(false, listOfA.isAssignableBy(ResolvedVoidType.INSTANCE));

        assertEquals(true, object.isAssignableBy(NullType.INSTANCE));
        assertEquals(true, string.isAssignableBy(NullType.INSTANCE));
        assertEquals(true, listOfStrings.isAssignableBy(NullType.INSTANCE));
        assertEquals(true, listOfA.isAssignableBy(NullType.INSTANCE));
    }

    @Test
    void testIsAssignableByBoxedPrimitive() {
        ResolvedReferenceType numberType =
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Number.class, typeSolver));
        ResolvedReferenceType intType =
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Integer.class, typeSolver));
        ResolvedReferenceType doubleType =
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Double.class, typeSolver));
        ResolvedReferenceType byteType = new ReferenceTypeImpl(new ReflectionClassDeclaration(Byte.class, typeSolver));
        ResolvedReferenceType shortType =
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Short.class, typeSolver));
        ResolvedReferenceType charType =
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Character.class, typeSolver));
        ResolvedReferenceType longType = new ReferenceTypeImpl(new ReflectionClassDeclaration(Long.class, typeSolver));
        ResolvedReferenceType booleanType =
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Boolean.class, typeSolver));
        ResolvedReferenceType floatType =
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Float.class, typeSolver));

        assertEquals(true, numberType.isAssignableBy(ResolvedPrimitiveType.INT));
        assertEquals(true, numberType.isAssignableBy(ResolvedPrimitiveType.DOUBLE));
        assertEquals(true, numberType.isAssignableBy(ResolvedPrimitiveType.SHORT));
        assertEquals(true, numberType.isAssignableBy(ResolvedPrimitiveType.LONG));
        assertEquals(true, numberType.isAssignableBy(ResolvedPrimitiveType.FLOAT));
        assertEquals(false, numberType.isAssignableBy(ResolvedPrimitiveType.BOOLEAN));
        assertEquals(true, intType.isAssignableBy(ResolvedPrimitiveType.INT));
        assertEquals(true, doubleType.isAssignableBy(ResolvedPrimitiveType.DOUBLE));
        assertEquals(true, byteType.isAssignableBy(ResolvedPrimitiveType.BYTE));
        assertEquals(true, shortType.isAssignableBy(ResolvedPrimitiveType.SHORT));
        assertEquals(true, charType.isAssignableBy(ResolvedPrimitiveType.CHAR));
        assertEquals(true, longType.isAssignableBy(ResolvedPrimitiveType.LONG));
        assertEquals(true, booleanType.isAssignableBy(ResolvedPrimitiveType.BOOLEAN));
        assertEquals(true, floatType.isAssignableBy(ResolvedPrimitiveType.FLOAT));
    }

    @Test
    void testIsCorresponding() {

        // ResolvedReferenceTypeTester is defined to allow to test protected method isCorrespondingBoxingType(..)
        class ResolvedReferenceTypeTester extends ReferenceTypeImpl {

            public ResolvedReferenceTypeTester(
                    ResolvedReferenceTypeDeclaration typeDeclaration, TypeSolver typeSolver) {
                super(typeDeclaration);
            }

            @Override
            public boolean isCorrespondingBoxingType(String name) {
                return super.isCorrespondingBoxingType(name);
            }
        }

        ResolvedReferenceTypeTester numberType =
                new ResolvedReferenceTypeTester(new ReflectionClassDeclaration(Number.class, typeSolver), typeSolver);
        ResolvedReferenceTypeTester intType =
                new ResolvedReferenceTypeTester(new ReflectionClassDeclaration(Integer.class, typeSolver), typeSolver);
        ResolvedReferenceTypeTester doubleType =
                new ResolvedReferenceTypeTester(new ReflectionClassDeclaration(Double.class, typeSolver), typeSolver);
        ResolvedReferenceTypeTester byteType =
                new ResolvedReferenceTypeTester(new ReflectionClassDeclaration(Byte.class, typeSolver), typeSolver);
        ResolvedReferenceTypeTester shortType =
                new ResolvedReferenceTypeTester(new ReflectionClassDeclaration(Short.class, typeSolver), typeSolver);
        ResolvedReferenceTypeTester charType = new ResolvedReferenceTypeTester(
                new ReflectionClassDeclaration(Character.class, typeSolver), typeSolver);
        ResolvedReferenceTypeTester longType =
                new ResolvedReferenceTypeTester(new ReflectionClassDeclaration(Long.class, typeSolver), typeSolver);
        ResolvedReferenceTypeTester booleanType =
                new ResolvedReferenceTypeTester(new ReflectionClassDeclaration(Boolean.class, typeSolver), typeSolver);
        ResolvedReferenceTypeTester floatType =
                new ResolvedReferenceTypeTester(new ReflectionClassDeclaration(Float.class, typeSolver), typeSolver);

        ResolvedReferenceTypeTester otherType =
                new ResolvedReferenceTypeTester(new ReflectionClassDeclaration(String.class, typeSolver), typeSolver);

        assertEquals(true, intType.isCorrespondingBoxingType(ResolvedPrimitiveType.INT.describe()));
        assertEquals(true, doubleType.isCorrespondingBoxingType(ResolvedPrimitiveType.DOUBLE.describe()));
        assertEquals(true, byteType.isCorrespondingBoxingType(ResolvedPrimitiveType.BYTE.describe()));
        assertEquals(true, shortType.isCorrespondingBoxingType(ResolvedPrimitiveType.SHORT.describe()));
        assertEquals(true, charType.isCorrespondingBoxingType(ResolvedPrimitiveType.CHAR.describe()));
        assertEquals(true, longType.isCorrespondingBoxingType(ResolvedPrimitiveType.LONG.describe()));
        assertEquals(true, booleanType.isCorrespondingBoxingType(ResolvedPrimitiveType.BOOLEAN.describe()));
        assertEquals(true, floatType.isCorrespondingBoxingType(ResolvedPrimitiveType.FLOAT.describe()));

        assertEquals(false, numberType.isCorrespondingBoxingType(ResolvedPrimitiveType.INT.describe()));

        assertThrows(IllegalArgumentException.class, () -> {
            intType.isCorrespondingBoxingType("String");
        });
    }

    @Test
    void testIsAssignableByGenerics() {
        assertEquals(false, listOfStrings.isAssignableBy(listOfWildcardExtendsString));
        assertEquals(false, listOfStrings.isAssignableBy(listOfWildcardExtendsString));
        assertEquals(true, listOfWildcardExtendsString.isAssignableBy(listOfStrings));
        assertEquals(false, listOfWildcardExtendsString.isAssignableBy(listOfWildcardSuperString));
        assertEquals(true, listOfWildcardSuperString.isAssignableBy(listOfStrings));
        assertEquals(false, listOfWildcardSuperString.isAssignableBy(listOfWildcardExtendsString));
    }

    @Test
    void testIsAssignableByGenericsInheritance() {
        assertEquals(true, collectionOfString.isAssignableBy(collectionOfString));
        assertEquals(true, collectionOfString.isAssignableBy(listOfStrings));
        assertEquals(true, collectionOfString.isAssignableBy(linkedListOfString));

        assertEquals(false, listOfStrings.isAssignableBy(collectionOfString));
        assertEquals(true, listOfStrings.isAssignableBy(listOfStrings));
        assertEquals(true, listOfStrings.isAssignableBy(linkedListOfString));

        assertEquals(false, linkedListOfString.isAssignableBy(collectionOfString));
        assertEquals(false, linkedListOfString.isAssignableBy(listOfStrings));
        assertEquals(true, linkedListOfString.isAssignableBy(linkedListOfString));
    }

    @Test
    void testIsAssignableByUnionType() {
        assertEquals(true, ioException.isAssignableBy(unionWithIOExceptionAsCommonAncestor));
        assertEquals(false, ioException.isAssignableBy(unionWithThrowableAsCommonAncestor));
    }

    @Test
    void testGetAllAncestorsConsideringTypeParameters() {
        assertThat(linkedListOfString.getAllAncestors(), hasItem(object));
        assertThat(linkedListOfString.getAllAncestors(), hasItem(listOfStrings));
        assertThat(linkedListOfString.getAllAncestors(), hasItem(collectionOfString));
        assertThat(linkedListOfString.getAllAncestors(), not(hasItem(listOfA)));
    }

    class Foo {}

    class Bar extends Foo {}

    class Bazzer<A, B, C> {}

    class MoreBazzing<A, B> extends Bazzer<B, String, A> {}

    @Test
    void testGetAllAncestorsConsideringGenericsCases() {
        ReferenceTypeImpl foo = new ReferenceTypeImpl(new ReflectionClassDeclaration(Foo.class, typeSolver));
        ReferenceTypeImpl bar = new ReferenceTypeImpl(new ReflectionClassDeclaration(Bar.class, typeSolver));
        ReferenceTypeImpl left, right;

        // YES MoreBazzing<Foo, Bar> e1 = new MoreBazzing<Foo, Bar>();
        assertEquals(
                true,
                new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(foo, bar))
                        .isAssignableBy(new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(foo, bar))));

        // YES MoreBazzing<? extends Foo, Bar> e2 = new MoreBazzing<Foo, Bar>();
        assertEquals(
                true,
                new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(ResolvedWildcard.extendsBound(foo), bar))
                        .isAssignableBy(new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(foo, bar))));

        // YES MoreBazzing<Foo, ? extends Bar> e3 = new MoreBazzing<Foo, Bar>();
        assertEquals(
                true,
                new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(foo, ResolvedWildcard.extendsBound(bar)))
                        .isAssignableBy(new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(foo, bar))));

        // YES MoreBazzing<? extends Foo, ? extends Foo> e4 = new MoreBazzing<Foo, Bar>();
        assertEquals(
                true,
                new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(
                                        ResolvedWildcard.extendsBound(foo), ResolvedWildcard.extendsBound(foo)))
                        .isAssignableBy(new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(foo, bar))));

        // YES MoreBazzing<? extends Foo, ? extends Foo> e5 = new MoreBazzing<Bar, Bar>();
        left = new ReferenceTypeImpl(
                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                ImmutableList.of(ResolvedWildcard.extendsBound(foo), ResolvedWildcard.extendsBound(foo)));
        right = new ReferenceTypeImpl(
                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver), ImmutableList.of(bar, bar));
        assertEquals(true, left.isAssignableBy(right));

        // YES Bazzer<Object, String, String> e6 = new MoreBazzing<String, Object>();
        left = new ReferenceTypeImpl(
                new ReflectionClassDeclaration(Bazzer.class, typeSolver), ImmutableList.of(object, string, string));
        right = new ReferenceTypeImpl(
                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver), ImmutableList.of(string, object));

        // To debug the following
        List<ResolvedReferenceType> ancestors = right.getAllAncestors();
        ResolvedReferenceType moreBazzingAncestor = ancestors.stream()
                .filter(a -> a.getQualifiedName().endsWith("Bazzer"))
                .findFirst()
                .get();

        assertEquals(true, left.isAssignableBy(right));

        // YES Bazzer<String,String,String> e7 = new MoreBazzing<String, String>();
        assertEquals(
                true,
                new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(Bazzer.class, typeSolver),
                                ImmutableList.of(string, string, string))
                        .isAssignableBy(new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(string, string))));

        // YES Bazzer<Bar,String,Foo> e8 = new MoreBazzing<Foo, Bar>();
        assertEquals(
                true,
                new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(Bazzer.class, typeSolver),
                                ImmutableList.of(bar, string, foo))
                        .isAssignableBy(new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(foo, bar))));

        // YES Bazzer<Foo,String,Bar> e9 = new MoreBazzing<Bar, Foo>();
        assertEquals(
                true,
                new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(Bazzer.class, typeSolver),
                                ImmutableList.of(foo, string, bar))
                        .isAssignableBy(new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(bar, foo))));

        // NO Bazzer<Bar,String,Foo> n1 = new MoreBazzing<Bar, Foo>();
        assertEquals(
                false,
                new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(Bazzer.class, typeSolver),
                                ImmutableList.of(bar, string, foo))
                        .isAssignableBy(new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(bar, foo))));

        // NO Bazzer<Bar,String,Bar> n2 = new MoreBazzing<Bar, Foo>();
        assertEquals(
                false,
                new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(Bazzer.class, typeSolver),
                                ImmutableList.of(bar, string, foo))
                        .isAssignableBy(new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(bar, foo))));

        // NO Bazzer<Foo,Object,Bar> n3 = new MoreBazzing<Bar, Foo>();
        assertEquals(
                false,
                new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(Bazzer.class, typeSolver),
                                ImmutableList.of(foo, object, bar))
                        .isAssignableBy(new ReferenceTypeImpl(
                                new ReflectionClassDeclaration(MoreBazzing.class, typeSolver),
                                ImmutableList.of(bar, foo))));
    }

    @Test
    void charSequenceIsAssignableToObject() {
        TypeSolver typeSolver = new ReflectionTypeSolver();
        ReferenceTypeImpl charSequence =
                new ReferenceTypeImpl(new ReflectionInterfaceDeclaration(CharSequence.class, typeSolver));
        ReferenceTypeImpl object = new ReferenceTypeImpl(new ReflectionClassDeclaration(Object.class, typeSolver));
        assertEquals(false, charSequence.isAssignableBy(object));
        assertEquals(true, object.isAssignableBy(charSequence));
    }

    @Test
    void testGetFieldTypeExisting() {
        class Foo<A> {

            List<A> elements;
        }

        TypeSolver typeSolver = new ReflectionTypeSolver();
        ReferenceTypeImpl ref = new ReferenceTypeImpl(new ReflectionClassDeclaration(Foo.class, typeSolver));

        assertEquals(true, ref.getFieldType("elements").isPresent());
        assertEquals(true, ref.getFieldType("elements").get().isReferenceType());
        assertEquals(
                List.class.getCanonicalName(),
                ref.getFieldType("elements").get().asReferenceType().getQualifiedName());
        assertEquals(
                1,
                ref.getFieldType("elements")
                        .get()
                        .asReferenceType()
                        .typeParametersValues()
                        .size());
        assertEquals(
                true,
                ref.getFieldType("elements")
                        .get()
                        .asReferenceType()
                        .typeParametersValues()
                        .get(0)
                        .isTypeVariable());
        assertEquals(
                "A",
                ref.getFieldType("elements")
                        .get()
                        .asReferenceType()
                        .typeParametersValues()
                        .get(0)
                        .asTypeParameter()
                        .getName());

        ref = new ReferenceTypeImpl(
                new ReflectionClassDeclaration(Foo.class, typeSolver),
                ImmutableList.of(new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeSolver))));

        assertEquals(true, ref.getFieldType("elements").isPresent());
        assertEquals(true, ref.getFieldType("elements").get().isReferenceType());
        assertEquals(
                List.class.getCanonicalName(),
                ref.getFieldType("elements").get().asReferenceType().getQualifiedName());
        assertEquals(
                1,
                ref.getFieldType("elements")
                        .get()
                        .asReferenceType()
                        .typeParametersValues()
                        .size());
        assertEquals(
                true,
                ref.getFieldType("elements")
                        .get()
                        .asReferenceType()
                        .typeParametersValues()
                        .get(0)
                        .isReferenceType());
        assertEquals(
                String.class.getCanonicalName(),
                ref.getFieldType("elements")
                        .get()
                        .asReferenceType()
                        .typeParametersValues()
                        .get(0)
                        .asReferenceType()
                        .getQualifiedName());
    }

    @Test
    void testGetFieldTypeUnexisting() {
        class Foo<A> {

            List<A> elements;
        }

        TypeSolver typeSolver = new ReflectionTypeSolver();
        ReferenceTypeImpl ref = new ReferenceTypeImpl(new ReflectionClassDeclaration(Foo.class, typeSolver));

        assertEquals(false, ref.getFieldType("bar").isPresent());

        ref = new ReferenceTypeImpl(
                new ReflectionClassDeclaration(Foo.class, typeSolver),
                ImmutableList.of(new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeSolver))));

        assertEquals(false, ref.getFieldType("bar").isPresent());
    }

    @Test
    void testTypeParamValue() {
        TypeSolver typeResolver = new ReflectionTypeSolver();
        ResolvedClassDeclaration arraylist = new ReflectionClassDeclaration(ArrayList.class, typeResolver);
        ResolvedClassDeclaration abstractList = new ReflectionClassDeclaration(AbstractList.class, typeResolver);
        ResolvedClassDeclaration abstractCollection =
                new ReflectionClassDeclaration(AbstractCollection.class, typeResolver);
        ResolvedInterfaceDeclaration list = new ReflectionInterfaceDeclaration(List.class, typeResolver);
        ResolvedInterfaceDeclaration collection = new ReflectionInterfaceDeclaration(Collection.class, typeResolver);
        ResolvedInterfaceDeclaration iterable = new ReflectionInterfaceDeclaration(Iterable.class, typeResolver);
        ResolvedType string = new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeResolver));
        ResolvedReferenceType arrayListOfString = new ReferenceTypeImpl(arraylist, ImmutableList.of(string));
        assertEquals(
                Optional.of(string),
                arrayListOfString.typeParamValue(arraylist.getTypeParameters().get(0)));
        assertEquals(
                Optional.of(string),
                arrayListOfString.typeParamValue(
                        abstractList.getTypeParameters().get(0)));
        assertEquals(
                Optional.of(string),
                arrayListOfString.typeParamValue(
                        abstractCollection.getTypeParameters().get(0)));
        assertEquals(
                Optional.of(string),
                arrayListOfString.typeParamValue(list.getTypeParameters().get(0)));
        assertEquals(
                Optional.of(string),
                arrayListOfString.typeParamValue(collection.getTypeParameters().get(0)));
        assertEquals(
                Optional.of(string),
                arrayListOfString.typeParamValue(iterable.getTypeParameters().get(0)));
    }

    @Test
    void testGetAllAncestorsOnRawType() {
        TypeSolver typeResolver = new ReflectionTypeSolver();
        ResolvedClassDeclaration arraylist = new ReflectionClassDeclaration(ArrayList.class, typeResolver);
        ResolvedReferenceType rawArrayList = new ReferenceTypeImpl(arraylist);

        Map<String, ResolvedReferenceType> ancestors = new HashMap<>();
        rawArrayList.getAllAncestors().forEach(a -> ancestors.put(a.getQualifiedName(), a));
        assertEquals(9, ancestors.size());

        ResolvedTypeVariable tv =
                new ResolvedTypeVariable(arraylist.getTypeParameters().get(0));
        assertEquals(
                new ReferenceTypeImpl(new ReflectionInterfaceDeclaration(RandomAccess.class, typeResolver)),
                ancestors.get("java.util.RandomAccess"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionClassDeclaration(AbstractCollection.class, typeResolver), ImmutableList.of(tv)),
                ancestors.get("java.util.AbstractCollection"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(List.class, typeResolver), ImmutableList.of(tv)),
                ancestors.get("java.util.List"));
        assertEquals(
                new ReferenceTypeImpl(new ReflectionInterfaceDeclaration(Cloneable.class, typeResolver)),
                ancestors.get("java.lang.Cloneable"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(Collection.class, typeResolver), ImmutableList.of(tv)),
                ancestors.get("java.util.Collection"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionClassDeclaration(AbstractList.class, typeResolver), ImmutableList.of(tv)),
                ancestors.get("java.util.AbstractList"));
        assertEquals(
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Object.class, typeResolver)),
                ancestors.get("java.lang.Object"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(Iterable.class, typeResolver), ImmutableList.of(tv)),
                ancestors.get("java.lang.Iterable"));
        assertEquals(
                new ReferenceTypeImpl(new ReflectionInterfaceDeclaration(Serializable.class, typeResolver)),
                ancestors.get("java.io.Serializable"));
    }

    @Test
    void testGetAllAncestorsOnTypeWithSpecifiedTypeParametersForInterface() {
        TypeSolver typeResolver = new ReflectionTypeSolver();
        ResolvedInterfaceDeclaration list = new ReflectionInterfaceDeclaration(List.class, typeResolver);
        ResolvedType string = new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeResolver));
        ResolvedReferenceType listOfString = new ReferenceTypeImpl(list, ImmutableList.of(string));

        Map<String, ResolvedReferenceType> ancestors = new HashMap<>();
        listOfString.getAllAncestors().forEach(a -> ancestors.put(a.getQualifiedName(), a));
        assertEquals(2, ancestors.size());

        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(Collection.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.util.Collection"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(Iterable.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.lang.Iterable"));
    }

    @Test
    void testGetAllAncestorsOnTypeWithSpecifiedTypeParametersForClassAbstractCollection() {
        TypeSolver typeResolver = new ReflectionTypeSolver();
        ResolvedClassDeclaration abstractCollection =
                new ReflectionClassDeclaration(AbstractCollection.class, typeResolver);
        ResolvedType string = new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeResolver));
        ResolvedReferenceType abstractCollectionOfString =
                new ReferenceTypeImpl(abstractCollection, ImmutableList.of(string));

        Map<String, ResolvedReferenceType> ancestors = new HashMap<>();
        abstractCollectionOfString.getAllAncestors().forEach(a -> ancestors.put(a.getQualifiedName(), a));
        assertEquals(3, ancestors.size());

        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(Collection.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.util.Collection"));
        assertEquals(
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Object.class, typeResolver)),
                ancestors.get("java.lang.Object"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(Iterable.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.lang.Iterable"));
    }

    @Test
    void testGetAllAncestorsOnTypeWithSpecifiedTypeParametersForClassAbstractList() {
        TypeSolver typeResolver = new ReflectionTypeSolver();
        ResolvedClassDeclaration abstractList = new ReflectionClassDeclaration(AbstractList.class, typeResolver);
        ResolvedType string = new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeResolver));
        ResolvedReferenceType abstractListOfString = new ReferenceTypeImpl(abstractList, ImmutableList.of(string));

        Map<String, ResolvedReferenceType> ancestors = new HashMap<>();
        abstractListOfString.getAllAncestors().forEach(a -> ancestors.put(a.getQualifiedName(), a));
        assertEquals(5, ancestors.size());

        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionClassDeclaration(AbstractCollection.class, typeResolver),
                        ImmutableList.of(string)),
                ancestors.get("java.util.AbstractCollection"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(List.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.util.List"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(Collection.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.util.Collection"));
        assertEquals(
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Object.class, typeResolver)),
                ancestors.get("java.lang.Object"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(Iterable.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.lang.Iterable"));
    }

    @Test
    void testGetAllAncestorsOnTypeWithSpecifiedTypeParametersForClassArrayList() {
        TypeSolver typeResolver = new ReflectionTypeSolver();
        ResolvedClassDeclaration arraylist = new ReflectionClassDeclaration(ArrayList.class, typeResolver);
        ResolvedType string = new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeResolver));
        ResolvedReferenceType arrayListOfString = new ReferenceTypeImpl(arraylist, ImmutableList.of(string));

        Map<String, ResolvedReferenceType> ancestors = new HashMap<>();
        arrayListOfString.getAllAncestors().forEach(a -> ancestors.put(a.getQualifiedName(), a));
        assertEquals(9, ancestors.size());

        assertEquals(
                new ReferenceTypeImpl(new ReflectionInterfaceDeclaration(RandomAccess.class, typeResolver)),
                ancestors.get("java.util.RandomAccess"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionClassDeclaration(AbstractCollection.class, typeResolver),
                        ImmutableList.of(string)),
                ancestors.get("java.util.AbstractCollection"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(List.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.util.List"));
        assertEquals(
                new ReferenceTypeImpl(new ReflectionInterfaceDeclaration(Cloneable.class, typeResolver)),
                ancestors.get("java.lang.Cloneable"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(Collection.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.util.Collection"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionClassDeclaration(AbstractList.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.util.AbstractList"));
        assertEquals(
                new ReferenceTypeImpl(new ReflectionClassDeclaration(Object.class, typeResolver)),
                ancestors.get("java.lang.Object"));
        assertEquals(
                new ReferenceTypeImpl(
                        new ReflectionInterfaceDeclaration(Iterable.class, typeResolver), ImmutableList.of(string)),
                ancestors.get("java.lang.Iterable"));
        assertEquals(
                new ReferenceTypeImpl(new ReflectionInterfaceDeclaration(Serializable.class, typeResolver)),
                ancestors.get("java.io.Serializable"));
    }

    @Test
    void testTypeParametersValues() {
        TypeSolver typeResolver = new ReflectionTypeSolver();
        ResolvedReferenceType stream =
                new ReferenceTypeImpl(new ReflectionInterfaceDeclaration(Stream.class, typeResolver));
        assertEquals(1, stream.typeParametersValues().size());
        assertEquals(
                new ResolvedTypeVariable(new ReflectionInterfaceDeclaration(Stream.class, typeResolver)
                        .getTypeParameters()
                        .get(0)),
                stream.typeParametersValues().get(0));
    }

    @Test
    void testReplaceTypeVariables() {
        TypeSolver typeResolver = new ReflectionTypeSolver();
        ResolvedInterfaceDeclaration streamInterface = new ReflectionInterfaceDeclaration(Stream.class, typeResolver);
        ResolvedReferenceType stream = new ReferenceTypeImpl(streamInterface);

        ResolvedMethodDeclaration streamMap = streamInterface.getDeclaredMethods().stream()
                .filter(m -> m.getName().equals("map"))
                .findFirst()
                .get();
        ResolvedTypeParameterDeclaration streamMapR =
                streamMap.findTypeParameter("T").get();
        ResolvedTypeVariable typeVariable = new ResolvedTypeVariable(streamMapR);
        stream = stream.deriveTypeParameters(stream.typeParametersMap().toBuilder()
                .setValue(stream.getTypeDeclaration().get().getTypeParameters().get(0), typeVariable)
                .build());

        ResolvedTypeParameterDeclaration tpToReplace =
                streamInterface.getTypeParameters().get(0);
        ResolvedType replaced = new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeResolver));

        ResolvedType streamReplaced = stream.replaceTypeVariables(tpToReplace, replaced);
        assertEquals("java.util.stream.Stream<java.lang.String>", streamReplaced.describe());
    }

    @Test
    void testReplaceTypeVariablesWithLambdaInBetween() {
        TypeSolver typeResolver = new ReflectionTypeSolver();
        ResolvedInterfaceDeclaration streamInterface = new ReflectionInterfaceDeclaration(Stream.class, typeResolver);
        ResolvedReferenceType stream = new ReferenceTypeImpl(streamInterface);

        ResolvedMethodDeclaration streamMap = streamInterface.getDeclaredMethods().stream()
                .filter(m -> m.getName().equals("map"))
                .findFirst()
                .get();
        ResolvedTypeParameterDeclaration streamMapR =
                streamMap.findTypeParameter("T").get();
        ResolvedTypeVariable typeVariable = new ResolvedTypeVariable(streamMapR);
        stream = stream.deriveTypeParameters(stream.typeParametersMap().toBuilder()
                .setValue(stream.getTypeDeclaration().get().getTypeParameters().get(0), typeVariable)
                .build());

        ResolvedTypeParameterDeclaration tpToReplace =
                streamInterface.getTypeParameters().get(0);
        ResolvedType replaced = new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeResolver));

        ResolvedType streamReplaced = stream.replaceTypeVariables(tpToReplace, replaced);
        assertEquals("java.util.stream.Stream<java.lang.String>", streamReplaced.describe());
    }

    @Test
    void testDirectAncestorsOfObject() {
        assertEquals(0, object.getDirectAncestors().size());
    }

    @Test
    void testDirectAncestorsOfInterface() {
        ResolvedReferenceType iterableOfString = new ReferenceTypeImpl(
                new ReflectionInterfaceDeclaration(Iterable.class, typeSolver),
                ImmutableList.of(new ReferenceTypeImpl(new ReflectionClassDeclaration(String.class, typeSolver))));
        assertEquals(0, iterableOfString.getDirectAncestors().size());
    }

    @Test
    void testDirectAncestorsOfInterfaceExtendingInterface() {
        assertEquals(1, collectionOfString.getDirectAncestors().size());
        ResolvedReferenceType ancestor1 =
                collectionOfString.getDirectAncestors().get(0);
        assertEquals("java.lang.Iterable", ancestor1.getQualifiedName());
        assertEquals(1, ancestor1.getTypeParametersMap().size());
        assertEquals("T", ancestor1.getTypeParametersMap().get(0).a.getName());
        assertEquals(
                "java.lang.String", ancestor1.getTypeParametersMap().get(0).b.describe());
    }

    @Test
    void testDirectAncestorsOfClassWithoutSuperClassOrInterfaces() {
        ResolvedReferenceType buffer = new ReferenceTypeImpl(new ReflectionClassDeclaration(Buffer.class, typeSolver));
        Set<String> ancestors = buffer.getDirectAncestors().stream()
                .map(ResolvedReferenceType::describe)
                .collect(Collectors.toSet());

        assertThat(ancestors, equalTo(new HashSet<>(Arrays.asList("java.lang.Object"))));
    }

    @Test
    void testDirectAncestorsOfObjectClass() {
        ResolvedReferenceType object = new ReferenceTypeImpl(new ReflectionClassDeclaration(Object.class, typeSolver));
        Set<String> ancestors = object.getDirectAncestors().stream()
                .map(ResolvedReferenceType::describe)
                .collect(Collectors.toSet());

        assertEquals(new HashSet<>(), ancestors);
    }

    @Test
    void testDirectAncestorsOfClassWithSuperClass() {
        ResolvedReferenceType charbuffer =
                new ReferenceTypeImpl(new ReflectionClassDeclaration(CharBuffer.class, typeSolver));
        Set<String> ancestors = charbuffer.getDirectAncestors().stream()
                .map(ResolvedReferenceType::describe)
                .collect(Collectors.toSet());

        assertThat(
                ancestors,
                containsInAnyOrder(
                        "java.lang.CharSequence",
                        "java.lang.Appendable",
                        "java.nio.Buffer",
                        "java.lang.Readable",
                        "java.lang.Comparable<java.nio.CharBuffer>"));
    }

    @Test
    void testDirectAncestorsOfClassWithInterfaces() {
        Set<String> ancestors = string.getDirectAncestors().stream()
                .map(ResolvedReferenceType::describe)
                .collect(Collectors.toSet());

        // FIXME: Remove this temporary fix which varies the test based on the detected JDK which is running these
        // tests.
        TestJdk currentJdk = TestJdk.getCurrentHostJdk();
        if (currentJdk.getMajorVersion() < 12) {
            // JDK 12 introduced "java.lang.constant.Constable"
            assertThat(
                    ancestors,
                    containsInAnyOrder(
                            "java.lang.CharSequence",
                            "java.lang.Object",
                            "java.lang.Comparable<java.lang.String>",
                            "java.io.Serializable"));
        } else {
            // JDK 12 introduced "java.lang.constant.Constable"
            assertThat(
                    ancestors,
                    containsInAnyOrder(
                            "java.lang.CharSequence",
                            "java.lang.Object",
                            "java.lang.Comparable<java.lang.String>",
                            "java.io.Serializable",
                            "java.lang.constant.Constable",
                            "java.lang.constant.ConstantDesc"));
        }
    }

    @Test
    void testDeclaredFields() {
        TypeSolver typeSolver = new ReflectionTypeSolver();
        String code =
                "class A { private int i; char c; public long l; } class B extends A { private float f; boolean b; };";
        ParserConfiguration parserConfiguration = new ParserConfiguration();
        parserConfiguration.setSymbolResolver(new JavaSymbolSolver(typeSolver));

        CompilationUnit cu =
                new JavaParser(parserConfiguration).parse(code).getResult().get();

        ClassOrInterfaceDeclaration classA = cu.getClassByName("A").get();
        ClassOrInterfaceDeclaration classB = cu.getClassByName("B").get();

        ResolvedReferenceType rtA = new ReferenceTypeImpl(classA.resolve());
        ResolvedReferenceType rtB = new ReferenceTypeImpl(classB.resolve());

        assertEquals(3, rtA.getDeclaredFields().size());
        assertTrue(rtA.getDeclaredFields().stream().anyMatch(f -> f.getName().equals("i")));
        assertTrue(rtA.getDeclaredFields().stream().anyMatch(f -> f.getName().equals("c")));
        assertTrue(rtA.getDeclaredFields().stream().anyMatch(f -> f.getName().equals("l")));

        assertEquals(2, rtB.getDeclaredFields().size());
        assertTrue(rtB.getDeclaredFields().stream().anyMatch(f -> f.getName().equals("f")));
        assertTrue(rtB.getDeclaredFields().stream().anyMatch(f -> f.getName().equals("b")));
    }

    @Test
    void testGetAllFieldsVisibleToInheritors() {
        TypeSolver typeSolver = new ReflectionTypeSolver();
        String code =
                "class A { private int i; char c; public long l; } class B extends A { private float f; boolean b; };";
        ParserConfiguration parserConfiguration = new ParserConfiguration();
        parserConfiguration.setSymbolResolver(new JavaSymbolSolver(typeSolver));

        CompilationUnit cu =
                new JavaParser(parserConfiguration).parse(code).getResult().get();

        ClassOrInterfaceDeclaration classA = cu.getClassByName("A").get();
        ClassOrInterfaceDeclaration classB = cu.getClassByName("B").get();

        ResolvedReferenceType rtA = new ReferenceTypeImpl(classA.resolve());
        ResolvedReferenceType rtB = new ReferenceTypeImpl(classB.resolve());

        assertEquals(2, rtA.getAllFieldsVisibleToInheritors().size());
        assertTrue(rtA.getAllFieldsVisibleToInheritors().stream()
                .anyMatch(f -> f.getName().equals("c")));
        assertTrue(rtA.getAllFieldsVisibleToInheritors().stream()
                .anyMatch(f -> f.getName().equals("l")));

        assertEquals(3, rtB.getAllFieldsVisibleToInheritors().size());
        assertTrue(rtB.getAllFieldsVisibleToInheritors().stream()
                .anyMatch(f -> f.getName().equals("c")));
        assertTrue(rtB.getAllFieldsVisibleToInheritors().stream()
                .anyMatch(f -> f.getName().equals("l")));
        assertTrue(rtB.getAllFieldsVisibleToInheritors().stream()
                .anyMatch(f -> f.getName().equals("b")));
    }

    @Test
    void erasure_non_generic_type() {
        List<ResolvedType> types = declaredTypes("class A {}");
        ResolvedType expected = types.get(0);
        assertEquals(expected, types.get(0).erasure());
    }

    @Test
    // The erasure of a parameterized type
    void erasure_rawtype() {
        List<ResolvedType> types = declaredTypes("class A<String> {}");
        ResolvedType rt = types.get(0);
        String expected = "A";
        ResolvedType erasedType = rt.erasure();
        assertFalse(rt.asReferenceType().isRawType());
        assertTrue(erasedType.asReferenceType().typeParametersValues().isEmpty());
        assertEquals(expected, erasedType.describe());
    }

    @Test
    // The erasure of a parameterized type with bound.
    void erasure_type_variable() {
        List<ResolvedType> types = declaredTypes("class A<T extends Number> {}");
        ResolvedType rt = types.get(0);
        String expected = "A";
        assertEquals(expected, rt.erasure().describe());
    }

    @Test
    // The erasure of a parameterized type
    void erasure_parametrizedType() {
        ResolvedType parametrizedType = genericType(
                Map.class.getCanonicalName(), Integer.class.getCanonicalName(), Integer.class.getCanonicalName());
        String expected = "java.util.Map";
        assertEquals(expected, parametrizedType.erasure().describe());
    }

    @Test
    // The erasure of an array type T[] is |T|[].
    void erasure_arraytype() {
        // create a type : List <String>[]
        ResolvedType genericList = array(genericType(List.class.getCanonicalName(), String.class.getCanonicalName()));
        String expected = "java.util.List[]";
        assertEquals(expected, genericList.erasure().describe());
    }

    @Test
    // The erasure of an array type T[] is |T|[].
    void erasure_arraytype_with_bound() {
        // create a type : List <T extends Serializable>[]
        ResolvedTypeVariable typeArguments = parametrizedType("T", Serializable.class.getCanonicalName());
        ResolvedType genericList = array(genericType(List.class.getCanonicalName(), typeArguments));
        String expected = "java.util.List[]";
        assertEquals(expected, genericList.erasure().describe());
    }

    @Test
    // The erasure of T extends Serializable is the erasure of its leftmost bound.
    void erasure_bounded_type_parameter() {
        // create a type : T extends Serializable
        ResolvedTypeVariable typeArguments = parametrizedType("T", Serializable.class.getCanonicalName());
        String expected = "java.io.Serializable";
        assertEquals(expected, typeArguments.erasure().describe());
    }

    @Test
    // The erasure of a nested type T.C is |T|.C.
    void erasure_nested_type() {
        List<ResolvedType> types =
                declaredTypes("class A<T> {" + "  class C{}" + "}", "class A {" + "  class C{}" + "}");
        ResolvedType typeA = types.get(0);
        ResolvedType typeC = types.get(1);
        // ResolvedType expectedErasedAType= types.get(2);
        ResolvedType expectedErasedCType = types.get(3);
        String expectedA = "A";
        String expectedC = "A.C";
        assertEquals(expectedA, typeA.erasure().describe());
        assertEquals(expectedC, typeC.erasure().describe());
        // this type declaration are not equals because the type returned by typeA.erasure() always contains original
        // typeParameters
        // assertEquals(expectedErasedAType, typeA.erasure());
        assertEquals(expectedErasedCType, typeC.erasure());
    }

    @Test
    void extend_type() {
        ResolvedTypeVariable variable = parametrizedType("java.util.List", "java.lang.String");
        assertTrue(variable.asTypeParameter().hasUpperBound());
        assertFalse(variable.asTypeParameter().hasLowerBound());
    }

    @Test
    void super_type() {
        ResolvedTypeVariable variable = parametrizedTypeLowerBounded("java.util.List", "java.lang.String");
        assertTrue(variable.asTypeParameter().hasLowerBound());
        assertFalse(variable.asTypeParameter().hasUpperBound());
    }

    // return a generic type with type arguments (arguments can be bounded)
    private ResolvedType genericType(String type, ResolvedType... parameterTypes) {
        return type(type, toList(parameterTypes));
    }

    // return a generic type with type arguments
    private ResolvedType genericType(String type, String... parameterTypes) {
        return new ReferenceTypeImpl(typeSolver.solveType(type), types(parameterTypes));
    }

    // return a list of types
    private List<ResolvedType> types(String... types) {
        return Arrays.stream(types).map(type -> type(type)).collect(Collectors.toList());
    }

    // return the specified type
    private ResolvedType type(String type) {
        return type(type, new ArrayList<>());
    }

    private ResolvedType type(String type, List<ResolvedType> typeArguments) {
        return new ReferenceTypeImpl(typeSolver.solveType(type), typeArguments);
    }

    // return a type parameter
    private ResolvedTypeVariable parametrizedType(String type, String parameterType) {
        return parametrizedTypeUpperBounded(type, parameterType);
    }

    private ResolvedTypeVariable parametrizedTypeUpperBounded(String type, String parameterType) {
        return new ResolvedTypeVariable(ResolvedTypeParameterDeclaration.onType(
                parameterType, type + "." + parameterType, Arrays.asList((extendBound(parameterType)))));
    }

    private ResolvedTypeVariable parametrizedTypeLowerBounded(String type, String parameterType) {
        return new ResolvedTypeVariable(ResolvedTypeParameterDeclaration.onType(
                parameterType, type + "." + parameterType, Arrays.asList((superBound(parameterType)))));
    }

    // return an extend bound
    private Bound extendBound(String type) {
        return Bound.extendsBound(type(type));
    }

    // return a super bound
    private Bound superBound(String type) {
        return Bound.superBound(type(type));
    }

    private Set<ResolvedType> toSet(ResolvedType... resolvedTypes) {
        return new HashSet<>(toList(resolvedTypes));
    }

    private List<ResolvedType> toList(ResolvedType... resolvedTypes) {
        return Arrays.asList(resolvedTypes);
    }

    // return an array type from the base type
    private ResolvedType array(ResolvedType baseType) {
        return new ResolvedArrayType(baseType);
    }

    // return a list of types from the declared types (using a static parser)
    private List<ResolvedType> declaredTypes(String... lines) {
        CompilationUnit tree = treeOf(lines);
        List<ResolvedType> results = Lists.newLinkedList();
        for (ClassOrInterfaceDeclaration classTree : tree.findAll(ClassOrInterfaceDeclaration.class)) {
            results.add(new ReferenceTypeImpl(classTree.resolve()));
        }
        return results;
    }

    private CompilationUnit treeOf(String... lines) {
        StringBuilder builder = new StringBuilder();
        for (String line : lines) {
            builder.append(line).append(System.lineSeparator());
        }
        return StaticJavaParser.parse(builder.toString());
    }
}
