/*
 * Copyright (C) 2007-2010 Júlio Vilmar Gesser.
 * Copyright (C) 2011, 2013-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;

import static com.github.javaparser.ParseStart.COMPILATION_UNIT;
import static com.github.javaparser.ParserConfiguration.LanguageLevel.BLEEDING_EDGE;
import static com.github.javaparser.ParserConfiguration.LanguageLevel.CURRENT;
import static com.github.javaparser.Providers.provider;
import static com.github.javaparser.Range.range;
import static com.github.javaparser.StaticJavaParser.*;
import static com.github.javaparser.utils.TestUtils.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.*;

import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.AnnotationMemberDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.*;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.stmt.ForStmt;
import com.github.javaparser.ast.stmt.ReturnStmt;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.github.javaparser.ast.type.IntersectionType;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.printer.YamlPrinter;
import com.github.javaparser.utils.LineSeparator;
import java.util.Optional;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class JavaParserTest {

    @BeforeEach
    void setToLatestJava() {
        StaticJavaParser.getConfiguration().setLanguageLevel(BLEEDING_EDGE);
    }

    @AfterEach
    void resetJavaLevel() {
        StaticJavaParser.getConfiguration().setLanguageLevel(CURRENT);
    }

    @Test
    void rangeOfAnnotationMemberDeclarationIsCorrect() {
        String code = "@interface AD { String foo(); }";
        CompilationUnit cu = parse(code);
        AnnotationMemberDeclaration memberDeclaration =
                cu.getAnnotationDeclarationByName("AD").get().getMember(0).asAnnotationMemberDeclaration();
        assertTrue(memberDeclaration.hasRange());
        assertEquals(
                new Range(new Position(1, 17), new Position(1, 29)),
                memberDeclaration.getRange().get());
    }

    @Test
    void testSourcePositionsWithUnicodeEscapes() {
        String code = "@interface AD \\u007B String foo(); \\u007D";
        CompilationUnit cu = parseWithUnicodeEscapes(code).getResult().get();
        AnnotationMemberDeclaration memberDeclaration =
                cu.getAnnotationDeclarationByName("AD").get().getMember(0).asAnnotationMemberDeclaration();
        assertTrue(memberDeclaration.hasRange());
        assertEquals(
                new Range(new Position(1, 22), new Position(1, 34)),
                memberDeclaration.getRange().get());
    }

    @Test
    void testSourcePositionsWithBrokenUnicodeEscapes() {
        // Source positions
        //                      111111111122222222 2 22333 3333
        //             123456789012345678901234567 8 90123 4567
        String code = "@interface AD { String X = \"\\uABC\"; }";
        ParseResult<CompilationUnit> cu = parseWithUnicodeEscapes(code);
        assertFalse(cu.getResult().isPresent());
        assertEquals(
                "Lexical error at line 1, column 34.  Encountered: \"\\\"\" (34), after : \"\\\"\\\\uABC\"",
                cu.getProblem(0).getMessage());
    }

    private static ParseResult<CompilationUnit> parseWithUnicodeEscapes(String code) {
        ParserConfiguration config = new ParserConfiguration();
        config.setPreprocessUnicodeEscapes(true);
        return new JavaParser(config).parse(code);
    }

    @Test
    void rangeOfAnnotationMemberDeclarationWithArrayTypeIsCorrect() {
        String code = "@interface AD { String[] foo(); }";
        CompilationUnit cu = parse(code);
        AnnotationMemberDeclaration memberDeclaration =
                cu.getAnnotationDeclarationByName("AD").get().getMember(0).asAnnotationMemberDeclaration();
        assertTrue(memberDeclaration.hasRange());
        assertEquals(
                new Range(new Position(1, 17), new Position(1, 31)),
                memberDeclaration.getRange().get());
    }

    @Test
    void rangeOfArrayCreationLevelWithExpressionIsCorrect() {
        String code = "new int[123][456]";
        ArrayCreationExpr expression = parseExpression(code);
        Optional<Range> range;

        range = expression.getLevels().get(0).getRange();
        assertTrue(range.isPresent());
        assertEquals(new Range(new Position(1, 8), new Position(1, 12)), range.get());

        range = expression.getLevels().get(1).getRange();
        assertTrue(range.isPresent());
        assertEquals(new Range(new Position(1, 13), new Position(1, 17)), range.get());
    }

    @Test
    void rangeOfArrayCreationLevelWithoutExpressionIsCorrect() {
        String code = "new int[][]";
        ArrayCreationExpr expression = parseExpression(code);
        Optional<Range> range;

        range = expression.getLevels().get(0).getRange();
        assertTrue(range.isPresent());
        assertEquals(new Range(new Position(1, 8), new Position(1, 9)), range.get());

        range = expression.getLevels().get(1).getRange();
        assertTrue(range.isPresent());
        assertEquals(new Range(new Position(1, 10), new Position(1, 11)), range.get());
    }

    @Test
    void parseErrorContainsLocation() {
        ParseResult<CompilationUnit> result = new JavaParser().parse(COMPILATION_UNIT, provider("class X { // blah"));

        Problem problem = result.getProblem(0);
        assertEquals(range(1, 9, 1, 17), problem.getLocation().get().toRange().get());
        assertEquals(
                "Parse error. Found <EOF>, expected one of  \";\" \"<\" \"@\" \"abstract\" \"boolean\" \"byte\" \"char\" \"class\" \"default\" \"double\" \"enum\" \"exports\" \"final\" \"float\" \"int\" \"interface\" \"long\" \"module\" \"native\" \"non-sealed\" \"open\" \"opens\" \"permits\" \"private\" \"protected\" \"provides\" \"public\" \"record\" \"requires\" \"sealed\" \"short\" \"static\" \"strictfp\" \"synchronized\" \"to\" \"transient\" \"transitive\" \"uses\" \"void\" \"volatile\" \"when\" \"with\" \"yield\" \"{\" \"}\" <IDENTIFIER>",
                problem.getMessage());
        assertInstanceOf(ParseException.class, problem.getCause().get());
    }

    @Test
    void parseIntersectionType() {
        String code = "(Runnable & Serializable) (() -> {})";
        Expression expression = parseExpression(code);
        Type type = expression.asCastExpr().getType();

        assertTrue(type instanceof IntersectionType);
        IntersectionType intersectionType = type.asIntersectionType();
        assertEquals(2, intersectionType.getElements().size());
        assertTrue(intersectionType.getElements().get(0) instanceof ClassOrInterfaceType);
        assertEquals(
                "Runnable",
                intersectionType.getElements().get(0).asClassOrInterfaceType().getNameAsString());
        assertTrue(intersectionType.getElements().get(1) instanceof ClassOrInterfaceType);
        assertEquals(
                "Serializable",
                intersectionType.getElements().get(1).asClassOrInterfaceType().getNameAsString());
    }

    @Test
    void parseArrayInitialization() {
        String code = "{1,2,3}";
        ArrayInitializerExpr expression = parseArrayInitializerExpr(code);

        assertEquals(3, expression.getValues().size());
    }

    @Test
    void rangeOfIntersectionType() {
        String code = "class A {" + LineSeparator.SYSTEM
                + "  Object f() {" + LineSeparator.SYSTEM
                + "    return (Comparator<Map.Entry<K, V>> & Serializable)(c1, c2) -> c1.getKey().compareTo(c2.getKey()); "
                + LineSeparator.SYSTEM
                + "}}";
        CompilationUnit cu = parse(code);
        MethodDeclaration methodDeclaration =
                cu.getClassByName("A").get().getMember(0).asMethodDeclaration();
        ReturnStmt returnStmt =
                methodDeclaration.getBody().get().getStatement(0).asReturnStmt();
        CastExpr castExpr = returnStmt.getExpression().get().asCastExpr();
        Type type = castExpr.getType();
        assertEquals(range(3, 13, 3, 54), type.getRange().get());
    }

    @Test
    void rangeOfCast() {
        String code = "class A {" + LineSeparator.SYSTEM
                + "  Object f() {" + LineSeparator.SYSTEM
                + "    return (Comparator<Map.Entry<K, V>> & Serializable)(c1, c2) -> c1.getKey().compareTo(c2.getKey()); "
                + LineSeparator.SYSTEM
                + "}}";
        CompilationUnit cu = parse(code);
        MethodDeclaration methodDeclaration =
                cu.getClassByName("A").get().getMember(0).asMethodDeclaration();
        ReturnStmt returnStmt =
                methodDeclaration.getBody().get().getStatement(0).asReturnStmt();
        CastExpr castExpr = returnStmt.getExpression().get().asCastExpr();
        assertEquals(range(3, 12, 3, 101), castExpr.getRange().get());
    }

    @Test
    void rangeOfCastNonIntersection() {
        String code = "class A {" + LineSeparator.SYSTEM
                + "  Object f() {" + LineSeparator.SYSTEM
                + "    return (Comparator<Map.Entry<K, V>>               )(c1, c2) -> c1.getKey().compareTo(c2.getKey()); "
                + LineSeparator.SYSTEM
                + "}}";
        CompilationUnit cu = parse(code);
        MethodDeclaration methodDeclaration =
                cu.getClassByName("A").get().getMember(0).asMethodDeclaration();
        ReturnStmt returnStmt =
                methodDeclaration.getBody().get().getStatement(0).asReturnStmt();
        CastExpr castExpr = returnStmt.getExpression().get().asCastExpr();
        assertEquals(range(3, 12, 3, 101), castExpr.getRange().get());
    }

    @Test
    void rangeOfLambda() {
        String code = "class A {" + LineSeparator.SYSTEM
                + "  Object f() {" + LineSeparator.SYSTEM
                + "    return (Comparator<Map.Entry<K, V>> & Serializable)(c1, c2) -> c1.getKey().compareTo(c2.getKey()); "
                + LineSeparator.SYSTEM
                + "}}";
        CompilationUnit cu = parse(code);
        MethodDeclaration methodDeclaration =
                cu.getClassByName("A").get().getMember(0).asMethodDeclaration();
        ReturnStmt returnStmt =
                methodDeclaration.getBody().get().getStatement(0).asReturnStmt();
        CastExpr castExpr = returnStmt.getExpression().get().asCastExpr();
        LambdaExpr lambdaExpr = castExpr.getExpression().asLambdaExpr();
        assertEquals(range(3, 56, 3, 101), lambdaExpr.getRange().get());
        assertEquals(
                GeneratedJavaParserConstants.LPAREN,
                lambdaExpr.getTokenRange().get().getBegin().getKind());
        assertEquals(
                GeneratedJavaParserConstants.RPAREN,
                lambdaExpr.getTokenRange().get().getEnd().getKind());
    }

    @Test
    void rangeOfLambdaBody() {
        String code = "class A {" + LineSeparator.SYSTEM
                + "  Object f() {" + LineSeparator.SYSTEM
                + "    return (Comparator<Map.Entry<K, V>> & Serializable)(c1, c2) -> c1.getKey().compareTo(c2.getKey()); "
                + LineSeparator.SYSTEM
                + "}}";
        CompilationUnit cu = parse(code);
        MethodDeclaration methodDeclaration =
                cu.getClassByName("A").get().getMember(0).asMethodDeclaration();
        ReturnStmt returnStmt =
                methodDeclaration.getBody().get().getStatement(0).asReturnStmt();
        CastExpr castExpr = returnStmt.getExpression().get().asCastExpr();
        LambdaExpr lambdaExpr = castExpr.getExpression().asLambdaExpr();
        Statement lambdaBody = lambdaExpr.getBody();
        assertEquals(range(3, 68, 3, 101), lambdaBody.getRange().get());
    }

    @Test
    void testNotStoringTokens() {
        JavaParser javaParser = new JavaParser(new ParserConfiguration().setStoreTokens(false));
        ParseResult<CompilationUnit> result = javaParser.parse(ParseStart.COMPILATION_UNIT, provider("class X{}"));
        assertFalse(result.getResult().get().getTokenRange().isPresent());
    }

    @Test
    void trailingCodeIsAnError() {
        assertThrows(ParseProblemException.class, () -> parseBlock("{} efijqoifjqefj"));
    }

    @Test
    void trailingWhitespaceIsIgnored() {
        BlockStmt blockStmt = parseBlock("{} // hello");
        assertEquals("{}", blockStmt.getTokenRange().get().toString());
    }

    @Test
    void parsingInitializedAndUnitializedVarsInForStmt() {
        ForStmt forStmt = parseStatement("for(int a,b=0;;){}").asForStmt();
        assertEquals(1, forStmt.getInitialization().size());
        assertTrue(forStmt.getInitialization().get(0).isVariableDeclarationExpr());
        assertEquals(
                2,
                forStmt.getInitialization()
                        .get(0)
                        .asVariableDeclarationExpr()
                        .getVariables()
                        .size());
        assertEquals(
                "a",
                forStmt.getInitialization()
                        .get(0)
                        .asVariableDeclarationExpr()
                        .getVariables()
                        .get(0)
                        .getNameAsString());
        assertEquals(
                "b",
                forStmt.getInitialization()
                        .get(0)
                        .asVariableDeclarationExpr()
                        .getVariables()
                        .get(1)
                        .getNameAsString());
        assertFalse(forStmt.getInitialization()
                .get(0)
                .asVariableDeclarationExpr()
                .getVariables()
                .get(0)
                .getInitializer()
                .isPresent());
        assertTrue(forStmt.getInitialization()
                .get(0)
                .asVariableDeclarationExpr()
                .getVariables()
                .get(1)
                .getInitializer()
                .isPresent());
    }

    @Test
    void parsingInitializedAndUnitializedVarsInForStmtComplexCase() {
        // See issue 1281
        ForStmt forStmt =
                parseStatement("for(int i, j = array2.length - 1;;){}").asForStmt();
        assertEquals(1, forStmt.getInitialization().size());
        assertTrue(forStmt.getInitialization().get(0).isVariableDeclarationExpr());
        assertEquals(
                2,
                forStmt.getInitialization()
                        .get(0)
                        .asVariableDeclarationExpr()
                        .getVariables()
                        .size());
        assertEquals(
                "i",
                forStmt.getInitialization()
                        .get(0)
                        .asVariableDeclarationExpr()
                        .getVariables()
                        .get(0)
                        .getNameAsString());
        assertEquals(
                "j",
                forStmt.getInitialization()
                        .get(0)
                        .asVariableDeclarationExpr()
                        .getVariables()
                        .get(1)
                        .getNameAsString());
        assertFalse(forStmt.getInitialization()
                .get(0)
                .asVariableDeclarationExpr()
                .getVariables()
                .get(0)
                .getInitializer()
                .isPresent());
        assertTrue(forStmt.getInitialization()
                .get(0)
                .asVariableDeclarationExpr()
                .getVariables()
                .get(1)
                .getInitializer()
                .isPresent());
    }

    @Test
    void creatingNewObjectCreationExprShouldDefaultToParsing() {
        String className = String.class.getCanonicalName();
        ClassOrInterfaceType type = parseClassOrInterfaceType(className);
        ObjectCreationExpr expected = parseExpression("new " + className + "()");
        ObjectCreationExpr actual = new ObjectCreationExpr(null, type, NodeList.nodeList());
        assertEquals(expected, actual);
    }

    @Test
    void parseModuleDeclaration() {
        StaticJavaParser.parseModuleDeclaration("module X {}");
    }

    @Test
    void parseModuleDirective() {
        StaticJavaParser.parseModuleDirective("opens C;");
    }

    @Test
    void parseTypeParameter() {
        StaticJavaParser.parseTypeParameter("T extends Serializable & AttachableListener");
    }

    @Test
    void parseTypeDeclaration() {
        StaticJavaParser.parseTypeDeclaration("enum Z {A, B}");
    }

    @Test
    void xxx() {
        YamlPrinter.print(StaticJavaParser.parse("class X{}"));
    }

    @Test
    void issue2879() {
        StaticJavaParser.parse("public class Test {" + "    public void method(int @MyAnno ... param) {}"
                + "}"
                + "@Target(java.lang.annotation.ElementType.TYPE_USE) @interface MyAnno {}");
    }
}
