/*
 * 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.resolution;

import static com.github.javaparser.StaticJavaParser.parse;
import static org.junit.jupiter.api.Assertions.*;

import com.github.javaparser.JavaParser;
import com.github.javaparser.JavaParserAdapter;
import com.github.javaparser.ParseStart;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.StringProvider;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.expr.ObjectCreationExpr;
import com.github.javaparser.ast.stmt.CatchClause;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.resolution.MethodUsage;
import com.github.javaparser.resolution.Navigator;
import com.github.javaparser.resolution.Solver;
import com.github.javaparser.resolution.TypeSolver;
import com.github.javaparser.resolution.declarations.ResolvedTypeDeclaration;
import com.github.javaparser.resolution.declarations.ResolvedValueDeclaration;
import com.github.javaparser.resolution.model.SymbolReference;
import com.github.javaparser.resolution.types.ResolvedReferenceType;
import com.github.javaparser.resolution.types.ResolvedType;
import com.github.javaparser.resolution.types.ResolvedUnionType;
import com.github.javaparser.symbolsolver.JavaSymbolSolver;
import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade;
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver;
import org.junit.jupiter.api.Test;

class JavaParserFacadeResolutionTest extends AbstractResolutionTest {

    @Test
    void typeDeclarationSuperClassImplicitlyIncludeObject() {
        CompilationUnit cu = parseSample("Generics");
        ClassOrInterfaceDeclaration clazz = Navigator.demandClass(cu, "Generics");
        ResolvedTypeDeclaration typeDeclaration =
                JavaParserFacade.get(new ReflectionTypeSolver()).getTypeDeclaration(clazz);
        ResolvedReferenceType superclass = typeDeclaration
                .asClass()
                .getSuperClass()
                .orElseThrow(() -> new RuntimeException("super class unexpectedly empty"));
        assertEquals(Object.class.getCanonicalName(), superclass.getQualifiedName());
    }

    // See issue 42
    @Test
    void solvingReferenceToUnsupportedOperationException() {
        String code = "public class Bla {\n" + "    public void main()\n"
                + "    {\n"
                + "        try\n"
                + "        {\n"
                + "            int i = 0;\n"
                + "        }\n"
                + "        catch (UnsupportedOperationException e)\n"
                + "        {\n"
                + "            String s;\n"
                + "            e.getMessage();\n"
                + "        }\n"
                + "    }\n"
                + "}";
        MethodCallExpr methodCallExpr = Navigator.demandNodeOfGivenClass(parse(code), MethodCallExpr.class);
        MethodUsage methodUsage =
                JavaParserFacade.get(new ReflectionTypeSolver()).solveMethodAsUsage(methodCallExpr);
        assertEquals("java.lang.Throwable.getMessage()", methodUsage.getQualifiedSignature());
    }

    // See issue 46
    @Test
    void solvingReferenceToCatchClauseParam() {
        String code = "public class Bla {\n" + "    public void main()\n"
                + "    {\n"
                + "        try\n"
                + "        {\n"
                + "            int i = 0;\n"
                + "        }\n"
                + "        catch (UnsupportedOperationException e)\n"
                + "        {\n"
                + "            String s;\n"
                + "            e.getMessage();\n"
                + "        }\n"
                + "    }\n"
                + "}";
        MethodCallExpr methodCallExpr = Navigator.demandNodeOfGivenClass(parse(code), MethodCallExpr.class);
        NameExpr nameE = (NameExpr) methodCallExpr.getScope().get();
        SymbolReference<? extends ResolvedValueDeclaration> symbolReference =
                JavaParserFacade.get(new ReflectionTypeSolver()).solve(nameE);
        assertTrue(symbolReference.isSolved());
        assertTrue(symbolReference.getCorrespondingDeclaration().isParameter());
        assertEquals(
                "e", symbolReference.getCorrespondingDeclaration().asParameter().getName());
        assertEquals(
                "java.lang.UnsupportedOperationException",
                symbolReference
                        .getCorrespondingDeclaration()
                        .asParameter()
                        .getType()
                        .asReferenceType()
                        .getQualifiedName());
    }

    // See issue 47
    @Test
    void solvingReferenceToAnAncestorInternalClass() {
        String code = "public class Foo {\n" + "    public class Base {\n"
                + "        public class X {\n"
                + "        }\n"
                + "    }\n"
                + "\n"
                + "    public class Derived extends Base {\n"
                + "        public X x = null;\n"
                + "    }\n"
                + "}";
        FieldDeclaration fieldDeclaration = Navigator.demandNodeOfGivenClass(parse(code), FieldDeclaration.class);
        Type jpType = fieldDeclaration.getCommonType();
        ResolvedType jssType = JavaParserFacade.get(new ReflectionTypeSolver()).convertToUsage(jpType);
        assertEquals("Foo.Base.X", jssType.asReferenceType().getQualifiedName());
    }

    // See issue 119
    @Test
    void solveTryWithResourceVariable() {
        String code = "import java.util.Scanner; class A { void foo() { try (Scanner sc = new Scanner(System.in)) {\n"
                + "    sc.nextLine();\n"
                + "} } }";
        CompilationUnit cu = parse(code);
        MethodCallExpr methodCallExpr = Navigator.findMethodCall(cu, "nextLine").get();
        Expression scope = methodCallExpr.getScope().get();
        ResolvedType type = JavaParserFacade.get(new ReflectionTypeSolver()).getType(scope);
        assertTrue(type.isReferenceType());
        assertEquals("java.util.Scanner", type.asReferenceType().getQualifiedName());
    }

    private CompilationUnit parseWithTypeSolver(String code) {
        TypeSolver typeSolver = new ReflectionTypeSolver();
        ParserConfiguration parserConfiguration = new ParserConfiguration();
        parserConfiguration.setSymbolResolver(new JavaSymbolSolver(typeSolver));
        JavaParser javaParser = new JavaParser(parserConfiguration);
        return javaParser
                .parse(ParseStart.COMPILATION_UNIT, new StringProvider(code))
                .getResult()
                .get();
    }

    @Test
    void solveMultiCatchType() {
        String code = "class A {\n" + "        public void foo() {\n"
                + "            try {\n"
                + "                \n"
                + "            } catch (IllegalStateException | IllegalArgumentException e) {\n"
                + "                \n"
                + "            }\n"
                + "        }\n"
                + "    }";
        CompilationUnit cu = parseWithTypeSolver(code);
        CatchClause catchClause = Navigator.demandNodeOfGivenClass(cu, CatchClause.class);
        Type jpType = catchClause.getParameter().getType();
        ResolvedType jssType = jpType.resolve();
        assertTrue(jssType instanceof ResolvedUnionType);
        assertTrue(jssType.asUnionType().getElements().size() == 2);
    }

    @Test
    void classToResolvedTypeViaReflection() {
        Class<?> clazz = this.getClass();
        Solver symbolSolver = new SymbolSolver(new ReflectionTypeSolver());
        ResolvedType resolvedType = symbolSolver.classToResolvedType(clazz);

        assertNotNull(resolvedType);
        assertTrue(resolvedType.isReferenceType());
        assertEquals(clazz.getCanonicalName(), resolvedType.asReferenceType().getQualifiedName());
    }

    // See issue 3725
    @Test
    void resolveVarTypeInForEachLoopFromArrayExpression() {
        String sourceCode = "" + "import java.util.Arrays;\n"
                + "\n"
                + "public class Main {\n"
                + "    public static void main(String[] args) {\n"
                + "        for (var s:args) {\n"
                + "            s.hashCode();\n"
                + "        }\n"
                + "    }\n"
                + "}";

        Expression toStringCallScope = scopeOfFirstHashCodeCall(sourceCode);

        // Before fixing the bug the next line failed with
        // "java.lang.IllegalStateException: Cannot resolve `var` which has no initializer."
        ResolvedType resolvedType = toStringCallScope.calculateResolvedType();

        assertEquals("java.lang.String", resolvedType.describe());
    }

    // See issue 3725
    @Test
    void resolveVarTypeInForEachLoopFromIterableExpression() {
        String sourceCode = "" + "import java.util.Arrays;\n"
                + "\n"
                + "public class Main {\n"
                + "    public static void main(String[] args) {\n"
                + "        for (var s: Arrays.asList(args)) {\n"
                + "            s.hashCode();\n"
                + "        }\n"
                + "    }\n"
                + "}";

        Expression toStringCallScope = scopeOfFirstHashCodeCall(sourceCode);

        // Before fixing the bug the next line failed with
        // "java.lang.IllegalStateException: Cannot resolve `var` which has no initializer."
        ResolvedType resolvedType = toStringCallScope.calculateResolvedType();

        assertEquals("java.lang.String", resolvedType.describe());
    }

    // See issue 3911
    @Test
    void resolveTypeParameterFromPrimitiveArrayArgument() {
        String sourceCode = "" + "import java.util.Arrays;\n"
                + "\n"
                + "public class Main {\n"
                + "    public void main(int[] args) {\n"
                + "        Arrays.asList(args);\n"
                + "    }\n"
                + "}";

        JavaParser parser = createParserWithResolver(defaultTypeSolver());
        CompilationUnit cu = parser.parse(sourceCode).getResult().get();

        MethodCallExpr mce = cu.findFirst(MethodCallExpr.class).get();

        ResolvedType resolvedType = mce.calculateResolvedType();

        assertEquals("java.util.List<int[]>", resolvedType.describe());
    }

    @Test
    void resolveTypeParameterFromReferenceArrayArgument() {
        String sourceCode = "" + "import java.util.Arrays;\n"
                + "\n"
                + "public class Main {\n"
                + "    public void main(String[] args) {\n"
                + "        Arrays.asList(args);\n"
                + "    }\n"
                + "}";

        JavaParser parser = createParserWithResolver(defaultTypeSolver());
        CompilationUnit cu = parser.parse(sourceCode).getResult().get();

        MethodCallExpr mce = cu.findFirst(MethodCallExpr.class).get();

        ResolvedType resolvedType = mce.calculateResolvedType();

        assertEquals("java.util.List<java.lang.String>", resolvedType.describe());
    }

    @Test
    void resolveTypeParameterFromPrimitiveArrayArgumentOnNonGenericExpectedParameter() {
        String sourceCode = "" + "import java.util.OptionalDouble;\n"
                + "import java.util.stream.IntStream;\n"
                + "\n"
                + "public class Main {\n"
                + "	OptionalDouble pre(int[] values) {\n"
                + "		return IntStream.of(values).map(s -> s).average();\n"
                + "	}\n"
                + "}";

        JavaParser parser = createParserWithResolver(defaultTypeSolver());
        CompilationUnit cu = parser.parse(sourceCode).getResult().get();

        MethodCallExpr mce = cu.findFirst(MethodCallExpr.class).get();

        ResolvedType resolvedType = mce.calculateResolvedType();

        assertEquals("java.util.OptionalDouble", resolvedType.describe());
    }

    @Test
    void resolveMethodTypeParametersUsingVariadicArgument() {
        String sourceCode = "import java.io.BufferedInputStream;\n" + "import java.io.IOException;\n"
                + "import java.nio.file.Files;\n"
                + "import java.nio.file.OpenOption;\n"
                + "import java.nio.file.Path;\n"
                + "\n"
                + "public class Test {\n"
                + "    public void write(final Path path, final OpenOption... options) throws IOException {\n"
                + "        BufferedInputStream in = new BufferedInputStream(Files.newInputStream(path, options));\n"
                + "    }\n"
                + "}";

        JavaParserAdapter parser = JavaParserAdapter.of(createParserWithResolver(defaultTypeSolver()));
        CompilationUnit cu = parser.parse(sourceCode);

        ObjectCreationExpr oce = cu.findFirst(ObjectCreationExpr.class).get();

        assertEquals("java.io.BufferedInputStream", oce.calculateResolvedType().describe());
    }

    // See issue 3725
    @Test
    void resolveVarTypeInForEachLoopFromIterableExpression2() {
        String sourceCode = "" + "import java.util.ArrayList;\n"
                + "import java.util.List;\n"
                + "\n"
                + "public class Main {\n"
                + "    public static void main(String[] args) {\n"
                + "        List<String> list = new ArrayList<>();"
                + "        for (var s: list) {\n"
                + "            s.hashCode();\n"
                + "        }\n"
                + "    }\n"
                + "}";

        Expression toStringCallScope = scopeOfFirstHashCodeCall(sourceCode);

        // Before fixing the bug the next line failed with
        // "java.lang.IllegalStateException: Cannot resolve `var` which has no initializer."
        ResolvedType resolvedType = toStringCallScope.calculateResolvedType();

        assertEquals("java.lang.String", resolvedType.describe());
    }

    // See issue 3725
    @Test
    void resolveVarTypeInForEachLoopFromIterableExpression_withRawType() {
        String sourceCode = "" + "import java.util.ArrayList;\n"
                + "import java.util.List;\n"
                + "\n"
                + "public class Main {\n"
                + "    public static void main(String[] args) {\n"
                + "        List list = new ArrayList();"
                + "        for (var s: list) {\n"
                + "            s.hashCode();\n"
                + "        }\n"
                + "    }\n"
                + "}";

        Expression toStringCallScope = scopeOfFirstHashCodeCall(sourceCode);

        ResolvedType resolvedType = toStringCallScope.calculateResolvedType();

        assertEquals("java.lang.Object", resolvedType.describe());
    }

    /**
     * Private helper method that returns the scope of the first
     * {@code hashCode} method call in the given sourceCode.
     * <p>
     * The sourceCode is processed with a Java 15 parser and a
     * ReflectionTypeSolver.
     */
    private static Expression scopeOfFirstHashCodeCall(String sourceCode) {
        // Parse the source code with Java 15 (and ReflectionTypeSolver)
        JavaSymbolSolver symbolResolver = new JavaSymbolSolver(new ReflectionTypeSolver());
        JavaParser parser = new JavaParser(new ParserConfiguration()
                .setSymbolResolver(symbolResolver)
                .setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_15));
        CompilationUnit cu = parser.parse(sourceCode).getResult().get();

        MethodCallExpr toStringCall = cu.findAll(MethodCallExpr.class).stream()
                .filter(mce -> mce.getNameAsString().equals("hashCode"))
                .findFirst()
                .get();
        return toStringCall.getScope().get();
    }
}
