///////////////////////////////////////////////////////////////////////////////////////////////
// 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.internal;

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

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
import com.puppycrawl.tools.checkstyle.Definitions;
import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocMethodCheck;
import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocStyleCheck;
import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocTypeCheck;
import com.puppycrawl.tools.checkstyle.checks.javadoc.WriteTagCheck;
import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;

public class XpathRegressionTest extends AbstractModuleTestSupport {

    // Checks that not compatible with SuppressionXpathFilter
    public static final Set<String> INCOMPATIBLE_CHECK_NAMES = Set.of(
            "NoCodeInFile (reason is that AST is not generated for a file not containing code)",
            "Regexp (reason is at  #7759)",
            "RegexpSinglelineJava (reason is at  #7759)"
    );

    // Javadoc checks are not compatible with SuppressionXpathFilter
    // till https://github.com/checkstyle/checkstyle/issues/5770
    // then all of them should be added to #INCOMPATIBLE_CHECK_NAMES
    // and this field should be removed
    public static final Set<String> INCOMPATIBLE_JAVADOC_CHECK_NAMES = Set.of(
                    "AtclauseOrder",
                    "JavadocBlockTagLocation",
                    "JavadocMethod",
                    "JavadocMissingLeadingAsterisk",
                    "JavadocMissingWhitespaceAfterAsterisk",
                    "JavadocParagraph",
                    "JavadocStyle",
                    "JavadocTagContinuationIndentation",
                    "JavadocType",
                    "MissingDeprecated",
                    "NonEmptyAtclauseDescription",
                    "RequireEmptyLineBeforeBlockTagGroup",
                    "SingleLineJavadoc",
                    "SummaryJavadoc",
                    "WriteTag"
    );

    // Older regex-based checks that are under INCOMPATIBLE_JAVADOC_CHECK_NAMES
    // but not subclasses of AbstractJavadocCheck.
    private static final Set<Class<?>> REGEXP_JAVADOC_CHECKS = Set.of(
                    JavadocStyleCheck.class,
                    JavadocMethodCheck.class,
                    JavadocTypeCheck.class,
                    WriteTagCheck.class
    );

    // Checks that allowed to have no XPath IT Regression Testing
    // till https://github.com/checkstyle/checkstyle/issues/6207
    private static final Set<String> MISSING_CHECK_NAMES = Set.of(
            "BooleanExpressionComplexity",
            "CatchParameterName",
            "ClassDataAbstractionCoupling",
            "ClassFanOutComplexity",
            "ClassTypeParameterName",
            "DescendantToken",
            "DesignForExtension",
            "EqualsHashCode",
            "ExecutableStatementCount",
            "FinalLocalVariable",
            "FinalParameters",
            "HideUtilityClassConstructor",
            "IllegalInstantiation",
            "IllegalTokenText",
            "InnerAssignment",
            "InnerTypeLast",
            "InterfaceTypeParameterName",
            "JavaNCSS",
            "LocalFinalVariableName",
            "LocalVariableName",
            "MagicNumber",
            "MethodLength",
            "MethodTypeParameterName",
            "ModifiedControlVariable",
            "MultipleStringLiterals",
            "MutableException",
            "PackageName",
            "ParameterAssignment",
            "ParameterNumber",
            "RedundantModifier",
            "SeparatorWrap",
            "SimplifyBooleanExpression",
            "StaticVariableName",
            "SuperClone",
            "SuperFinalize",
            "SuppressWarnings",
            "VisibilityModifier"
    );

    // Modules that will never have xpath support ever because they not report violations
    private static final Set<String> NO_VIOLATION_MODULES = Set.of(
            "SuppressWarningsHolder"
    );

    private static final Set<String> SIMPLE_CHECK_NAMES = getSimpleCheckNames();
    private static final Map<String, String> ALLOWED_DIRECTORY_AND_CHECKS =
        getAllowedDirectoryAndChecks();

    private static final Set<String> INTERNAL_MODULES = getInternalModules();

    private Path javaDir;
    private Path inputDir;

    private static Set<String> getSimpleCheckNames() {
        try {
            return CheckUtil.getSimpleNames(CheckUtil.getCheckstyleChecks());
        }
        catch (IOException ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }

    private static Map<String, String> getAllowedDirectoryAndChecks() {
        return SIMPLE_CHECK_NAMES
            .stream()
            .collect(Collectors.toMap(id -> id.toLowerCase(Locale.ENGLISH), Function.identity()));
    }

    private static Set<String> getInternalModules() {
        return Definitions.INTERNAL_MODULES.stream()
            .map(moduleName -> {
                final String[] packageTokens = moduleName.split("\\.");
                return packageTokens[packageTokens.length - 1];
            })
            .collect(Collectors.toSet());
    }

    @BeforeEach
    public void setUp() throws Exception {
        javaDir = Paths.get("src/it/java/" + getPackageLocation());
        inputDir = Paths.get(getPath(""));
    }

    @Override
    protected String getPackageLocation() {
        return "org/checkstyle/suppressionxpathfilter";
    }

    @Override
    protected String getResourceLocation() {
        return "it";
    }

    @Test
    public void validateIncompatibleJavadocCheckNames() throws IOException {
        // subclasses of AbstractJavadocCheck
        final Set<Class<?>> abstractJavadocCheckNames = CheckUtil.getCheckstyleChecks()
                .stream()
                .filter(AbstractJavadocCheck.class::isAssignableFrom)
                .collect(Collectors.toCollection(HashSet::new));
        // add the extra checks
        abstractJavadocCheckNames.addAll(REGEXP_JAVADOC_CHECKS);
        final Set<String> abstractJavadocCheckSimpleNames =
                CheckUtil.getSimpleNames(abstractJavadocCheckNames);
        abstractJavadocCheckSimpleNames.removeAll(INTERNAL_MODULES);
        assertWithMessage("INCOMPATIBLE_JAVADOC_CHECK_NAMES should contains all descendants "
                    + "of AbstractJavadocCheck")
            .that(abstractJavadocCheckSimpleNames)
            .isEqualTo(INCOMPATIBLE_JAVADOC_CHECK_NAMES);
    }

    @Test
    public void validateIntegrationTestClassNames() throws Exception {
        final Set<String> compatibleChecks = new HashSet<>();
        final Pattern pattern = Pattern.compile("^XpathRegression(.+)Test\\.java$");
        try (DirectoryStream<Path> javaPaths = Files.newDirectoryStream(javaDir)) {
            for (Path path : javaPaths) {
                assertWithMessage(path + " is not a regular file")
                        .that(Files.isRegularFile(path))
                        .isTrue();
                final String filename = path.toFile().getName();
                if (filename.startsWith("Abstract")) {
                    continue;
                }

                final Matcher matcher = pattern.matcher(filename);
                assertWithMessage(
                            "Invalid test file: " + filename + ", expected pattern: " + pattern)
                        .that(matcher.matches())
                        .isTrue();

                final String check = matcher.group(1);
                assertWithMessage("Unknown check '" + check + "' in test file: " + filename)
                        .that(SIMPLE_CHECK_NAMES)
                        .contains(check);

                assertWithMessage(
                            "Check '" + check + "' is now tested. Please update the todo list in"
                                + " XpathRegressionTest.MISSING_CHECK_NAMES")
                        .that(MISSING_CHECK_NAMES.contains(check))
                        .isFalse();
                assertWithMessage(
                            "Check '" + check + "' is now compatible with SuppressionXpathFilter."
                                + " Please update the todo list in"
                                + " XpathRegressionTest.INCOMPATIBLE_CHECK_NAMES")
                        .that(INCOMPATIBLE_CHECK_NAMES.contains(check))
                        .isFalse();
                compatibleChecks.add(check);
            }
        }

        // Ensure that all lists are up-to-date
        final Set<String> allChecks = new HashSet<>(SIMPLE_CHECK_NAMES);
        allChecks.removeAll(INCOMPATIBLE_JAVADOC_CHECK_NAMES);
        allChecks.removeAll(INCOMPATIBLE_CHECK_NAMES);
        allChecks.removeAll(Set.of("Regexp", "RegexpSinglelineJava", "NoCodeInFile"));
        allChecks.removeAll(MISSING_CHECK_NAMES);
        allChecks.removeAll(NO_VIOLATION_MODULES);
        allChecks.removeAll(compatibleChecks);
        allChecks.removeAll(INTERNAL_MODULES);

        assertWithMessage("XpathRegressionTest is missing for [" + String.join(", ", allChecks)
                + "]. Please add them to src/it/java/org/checkstyle/suppressionxpathfilter")
                        .that(allChecks)
                        .isEmpty();
    }

    @Test
    public void validateInputFiles() throws Exception {
        try (DirectoryStream<Path> dirs = Files.newDirectoryStream(inputDir)) {
            for (Path dir : dirs) {
                // input directory must be named in lower case
                assertWithMessage(dir + " is not a directory")
                        .that(Files.isDirectory(dir))
                        .isTrue();
                final String dirName = dir.toFile().getName();
                assertWithMessage("Invalid directory name: " + dirName)
                        .that(ALLOWED_DIRECTORY_AND_CHECKS)
                        .containsKey(dirName);

                // input directory must be connected to an existing test
                final String check = ALLOWED_DIRECTORY_AND_CHECKS.get(dirName);
                final Path javaPath = javaDir.resolve("XpathRegression" + check + "Test.java");
                assertWithMessage("Input directory '" + dir
                            + "' is not connected to Java test case: " + javaPath)
                        .that(Files.exists(javaPath))
                        .isTrue();

                // input files should be named correctly
                validateInputDirectory(dir);
            }
        }
    }

    private static void validateInputDirectory(Path checkDir) throws IOException {
        final Pattern pattern = Pattern.compile("^SuppressionXpathRegression(.+)\\.java$");
        final String check = ALLOWED_DIRECTORY_AND_CHECKS.get(checkDir.toFile().getName());

        try (DirectoryStream<Path> inputPaths = Files.newDirectoryStream(checkDir)) {
            for (Path inputPath : inputPaths) {
                final String filename = inputPath.toFile().getName();
                if (filename.endsWith("java")) {
                    final Matcher matcher = pattern.matcher(filename);
                    assertWithMessage(
                              "Invalid input file '" + inputPath + "', expected pattern:" + pattern)
                            .that(matcher.matches())
                            .isTrue();

                    final String remaining = matcher.group(1);
                    assertWithMessage("Check name '" + check
                                + "' should be included in input file: " + inputPath)
                            .that(remaining)
                            .startsWith(check);
                }
            }
        }
    }
}
