///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2023 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library 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.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
///////////////////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle;

import static com.google.common.truth.Truth.assertWithMessage;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.Test;

import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;

public class JavaParserTest extends AbstractModuleTestSupport {

    @Override
    protected String getPackageLocation() {
        return "com/puppycrawl/tools/checkstyle/javaparser";
    }

    @Test
    public void testIsProperUtilsClass() throws ReflectiveOperationException {
        assertWithMessage("Constructor is not private")
                .that(TestUtil.isUtilsClassHasPrivateConstructor(JavaParser.class))
                .isTrue();
    }

    @Test
    public void testNullRootWithComments() {
        assertWithMessage("Invalid return root")
            .that(JavaParser.appendHiddenCommentNodes(null))
            .isNull();
    }

    @Test
    public void testAppendHiddenBlockCommentNodes() throws Exception {
        final DetailAST root =
            JavaParser.parseFile(new File(getPath("InputJavaParserHiddenComments.java")),
                JavaParser.Options.WITH_COMMENTS);

        final Optional<DetailAST> blockComment = TestUtil.findTokenInAstByPredicate(root,
            ast -> ast.getType() == TokenTypes.BLOCK_COMMENT_BEGIN);

        assertWithMessage("Block comment should be present")
                .that(blockComment.isPresent())
                .isTrue();

        final DetailAST comment = blockComment.get();

        assertWithMessage("Unexpected line number")
            .that(comment.getLineNo())
            .isEqualTo(3);
        assertWithMessage("Unexpected column number")
            .that(comment.getColumnNo())
            .isEqualTo(0);
        assertWithMessage("Unexpected comment content")
            .that(comment.getText())
            .isEqualTo("/*");

        final DetailAST commentContent = comment.getFirstChild();
        final DetailAST commentEnd = comment.getLastChild();

        assertWithMessage("Unexpected line number")
            .that(commentContent.getLineNo())
            .isEqualTo(3);
        assertWithMessage("Unexpected column number")
            .that(commentContent.getColumnNo())
            .isEqualTo(2);
        assertWithMessage("Unexpected line number")
            .that(commentEnd.getLineNo())
            .isEqualTo(9);
        assertWithMessage("Unexpected column number")
            .that(commentEnd.getColumnNo())
            .isEqualTo(1);
    }

    @Test
    public void testAppendHiddenSingleLineCommentNodes() throws Exception {
        final DetailAST root =
            JavaParser.parseFile(new File(getPath("InputJavaParserHiddenComments.java")),
                JavaParser.Options.WITH_COMMENTS);

        final Optional<DetailAST> singleLineComment = TestUtil.findTokenInAstByPredicate(root,
            ast -> ast.getType() == TokenTypes.SINGLE_LINE_COMMENT);
        assertWithMessage("Single line comment should be present")
                .that(singleLineComment.isPresent())
                .isTrue();

        final DetailAST comment = singleLineComment.get();

        assertWithMessage("Unexpected line number")
            .that(comment.getLineNo())
            .isEqualTo(13);
        assertWithMessage("Unexpected column number")
            .that(comment.getColumnNo())
            .isEqualTo(0);
        assertWithMessage("Unexpected comment content")
            .that(comment.getText())
            .isEqualTo("//");

        final DetailAST commentContent = comment.getFirstChild();

        assertWithMessage("Unexpected token type")
            .that(commentContent.getType())
            .isEqualTo(TokenTypes.COMMENT_CONTENT);
        assertWithMessage("Unexpected line number")
            .that(commentContent.getLineNo())
            .isEqualTo(13);
        assertWithMessage("Unexpected column number")
            .that(commentContent.getColumnNo())
            .isEqualTo(2);
        assertWithMessage("Unexpected comment content")
                .that(commentContent.getText())
                .startsWith(" inline comment");
    }

    @Test
    public void testAppendHiddenSingleLineCommentNodes2() throws Exception {
        final DetailAST root =
            JavaParser.parseFile(new File(getPath("InputJavaParserHiddenComments2.java")),
                JavaParser.Options.WITH_COMMENTS);

        final Optional<DetailAST> singleLineComment = TestUtil.findTokenInAstByPredicate(root,
            ast -> ast.getType() == TokenTypes.SINGLE_LINE_COMMENT);
        assertWithMessage("Single line comment should be present")
                .that(singleLineComment.isPresent())
                .isTrue();

        final DetailAST comment = singleLineComment.get();

        assertWithMessage("Unexpected line number")
            .that(comment.getLineNo())
            .isEqualTo(1);
        assertWithMessage("Unexpected column number")
            .that(comment.getColumnNo())
            .isEqualTo(4);
        assertWithMessage("Unexpected comment content")
            .that(comment.getText())
            .isEqualTo("//");

        final DetailAST commentContent = comment.getFirstChild();

        assertWithMessage("Unexpected token type")
            .that(commentContent.getType())
            .isEqualTo(TokenTypes.COMMENT_CONTENT);
        assertWithMessage("Unexpected line number")
            .that(commentContent.getLineNo())
            .isEqualTo(1);
        assertWithMessage("Unexpected column number")
            .that(commentContent.getColumnNo())
            .isEqualTo(6);
        assertWithMessage("Unexpected comment content")
                .that(commentContent.getText())
                .startsWith(" indented comment");
    }

    @Test
    public void testDontAppendCommentNodes() throws Exception {
        final DetailAST root =
            JavaParser.parseFile(new File(getPath("InputJavaParserHiddenComments.java")),
                JavaParser.Options.WITHOUT_COMMENTS);

        final Optional<DetailAST> singleLineComment = TestUtil.findTokenInAstByPredicate(root,
            ast -> ast.getType() == TokenTypes.SINGLE_LINE_COMMENT);
        assertWithMessage("Single line comment should be present")
                .that(singleLineComment.isPresent())
                .isFalse();
    }

    @Test
    public void testParseException() throws Exception {
        final File input = new File(getNonCompilablePath("InputJavaParser.java"));
        try {
            JavaParser.parseFile(input, JavaParser.Options.WITH_COMMENTS);
            assertWithMessage("exception expected").fail();
        }
        catch (CheckstyleException ex) {
            assertWithMessage("Invalid exception message")
                .that(ex.toString())
                .isEqualTo(CheckstyleException.class.getName()
                            + ": IllegalStateException occurred while parsing file "
                            + input.getAbsolutePath() + ".");
            assertWithMessage("Invalid class")
                .that(ex.getCause())
                .isInstanceOf(IllegalStateException.class);
            assertWithMessage("Invalid exception message")
                .that(ex.getCause().toString())
                .isEqualTo(IllegalStateException.class.getName()
                            + ": 2:0: no viable alternative at input 'classD'");
        }
    }

    @Test
    public void testComments() throws Exception {
        final DetailAST root =
            JavaParser.parseFile(new File(getPath("InputJavaParserHiddenComments3.java")),
                JavaParser.Options.WITH_COMMENTS);
        final CountComments counter = new CountComments(root);

        assertWithMessage("Invalid line comments")
            .that(counter.lineComments)
            .isEqualTo(Arrays.asList("1,4", "6,4", "9,0"));
        assertWithMessage("Invalid block comments")
            .that(counter.blockComments)
            .isEqualTo(Arrays.asList("5,4", "8,0"));
    }

    @Test
    public void testJava14TextBlocks() throws Exception {
        final DetailAST root =
            JavaParser.parseFile(new File(
                    getNonCompilablePath("InputJavaParserTextBlocks.java")),
                JavaParser.Options.WITHOUT_COMMENTS);

        final Optional<DetailAST> textBlockContent = TestUtil.findTokenInAstByPredicate(root,
            ast -> ast.getType() == TokenTypes.TEXT_BLOCK_CONTENT);

        assertWithMessage("Text block content should be present")
                .that(textBlockContent.isPresent())
                .isTrue();

        final DetailAST content = textBlockContent.get();
        final String expectedContents = "\n                 string";

        assertWithMessage("Unexpected line number")
            .that(content.getLineNo())
            .isEqualTo(5);
        assertWithMessage("Unexpected column number")
            .that(content.getColumnNo())
            .isEqualTo(32);
        assertWithMessage("Unexpected text block content")
            .that(content.getText())
            .isEqualTo(expectedContents);
    }

    @Test
    public void testNoFreezeOnDeeplyNestedLambdas() throws Exception {
        final File file =
                new File(getNonCompilablePath("InputJavaParserNoFreezeOnDeeplyNestedLambdas.java"));
        assertWithMessage("File parsing should complete successfully.")
                .that(JavaParser.parseFile(file, JavaParser.Options.WITH_COMMENTS))
                .isNotNull();
    }

    @Test
    public void testFullJavaIdentifierSupport() throws Exception {
        final File file =
                new File(getNonCompilablePath("InputJavaParserFullJavaIdentifierSupport.java"));
        assertWithMessage("File parsing should complete successfully.")
                .that(JavaParser.parseFile(file, JavaParser.Options.WITH_COMMENTS))
                .isNotNull();
    }

    private static final class CountComments {
        private final List<String> lineComments = new ArrayList<>();
        private final List<String> blockComments = new ArrayList<>();

        private CountComments(DetailAST root) {
            forEachChild(root);
        }

        private void forEachChild(DetailAST root) {
            for (DetailAST ast = root; ast != null; ast = ast.getNextSibling()) {
                if (ast.getType() == TokenTypes.SINGLE_LINE_COMMENT) {
                    lineComments.add(ast.getLineNo() + "," + ast.getColumnNo());
                }
                else if (ast.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
                    blockComments.add(ast.getLineNo() + "," + ast.getColumnNo());
                }

                forEachChild(ast.getFirstChild());
            }
        }
    }
}
