/*
 * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8224613
 * @library /tools/lib ../../lib
 * @modules jdk.javadoc/jdk.javadoc.internal.tool
 * @build javadoc.tester.* toolbox.ToolBox builder.ClassBuilder
 * @run main/othervm OptionProcessingFailureTest
 */

import builder.ClassBuilder;
import javadoc.tester.JavadocTester;
import jdk.javadoc.doclet.Doclet;
import jdk.javadoc.doclet.DocletEnvironment;
import jdk.javadoc.doclet.Reporter;
import toolbox.ToolBox;

import javax.lang.model.SourceVersion;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/*
 * Tests that a particular error is raised only if a doclet has not reported any
 * errors (to Reporter), and yet at least one of that Doclet's options returned
 * `false` from the `Doclet.Option.process` method.
 *
 * This test explores scenarios generated by combining a few independent factors
 * involved in failing a doclet run. These factors are:
 *
 *   1. Reporting errors in Doclet.init(...)
 *   2. Reporting errors in Doclet.Option.process(...)
 *   3. Returning `false` from Doclet.Option.process(...)
 *
 * A doclet that behaves according to a scenario is run by the javadoc tool. An
 * output of that run is then examined for presence of a particular error. That
 * error must be in the output if and only if one or more options returned
 * `false` from Doclet.Option.process(...) and no other errors were reported.
 *
 * Configuration of the doclet is performed out-of-band, using System Properties
 * (when running the javadoc tool from the command line this could be achieved
 * using -J-Dsystem.property.name=value syntax). The "puppet" doclet is
 * instructed on which options is has, how many errors it should report,
 * and how each individual option should be processed.
 */
public class OptionProcessingFailureTest extends JavadocTester {

    private final ToolBox tb = new ToolBox();

    public static void main(String... args) throws Exception {
        new OptionProcessingFailureTest().runTests(m -> new Object[]{Paths.get(m.getName())});
    }

    @Test
    public void test(Path base) throws IOException {
        Path srcDir = base.resolve("src");

        // Since the minimal successful run of the javadoc tool with a custom
        // doclet involves source files, a package with a class in it is generated:
        new ClassBuilder(tb, "pkg.A")
                .setModifiers("public", "class")
                .write(srcDir);

        generateScenarios(base, this::testScenario);
    }

    private static void generateScenarios(Path base, ScenarioConsumer consumer) {
        for (int nInitErrors : List.of(0, 1)) { // the number of errors the Doclet.init method should report
            for (int nOptions : List.of(0, 1, 2)) { // the number of options a doclet should have
                generateOptionsCombinations(base, nInitErrors, nOptions, consumer);
            }
        }
    }

    private static void generateOptionsCombinations(Path base,
                                                    int nInitErrors,
                                                    int nOptions,
                                                    ScenarioConsumer consumer) {
        var emptyDescriptions = new PuppetOption.Description[nOptions];
        generateOptionsCombinations(base, nInitErrors, 0, emptyDescriptions, consumer);
    }


    private static void generateOptionsCombinations(Path base,
                                                    int nInitErrors,
                                                    int descriptionsIndex,
                                                    PuppetOption.Description[] descriptions,
                                                    ScenarioConsumer consumer) {
        if (descriptionsIndex >= descriptions.length) {
            consumer.consume(base, nInitErrors, List.of(descriptions));
            return;
        }
        for (boolean success : List.of(true, false)) { // return values of Doclet.Options.process
            for (int nErrors : List.of(0, 1)) { // the number of errors Doclet.Options.process should report
                String optionName = "--doclet-option-" + descriptionsIndex;
                descriptions[descriptionsIndex] = new PuppetOption.Description(optionName, success, nErrors);
                generateOptionsCombinations(base, nInitErrors, descriptionsIndex + 1, descriptions, consumer);
            }
        }
    }

    private void testScenario(Path base,
                              int nInitErrors,
                              List<PuppetOption.Description> optionDescriptions) {
        System.out.printf("nInitErrors=%s, optionDescriptions=%s%n", nInitErrors, optionDescriptions);

        List<String> args = new ArrayList<>(
                List.of("-doclet", PuppetDoclet.class.getName(),
                        "-docletpath", System.getProperty("test.classes", "."),
                        "-sourcepath", base.resolve("src").toString(),
                        "pkg"));

        optionDescriptions.forEach(d -> args.add(d.name)); // options passed to the doclet

        /* The puppet doclet does not create any output directories, so there is
           no need for any related checks; other checks are disabled to speed up
           the processing and reduce the size of the output. */

        setOutputDirectoryCheck(DirectoryCheck.NONE);
        setAutomaticCheckAccessibility(false);
        setAutomaticCheckLinks(false);

        /* Ideally, the system properties should've been passed to the `javadoc`
           method below. However, since there's no such option, the system
           properties are manipulated in an external fashion. */

        String descriptions = System.getProperty("puppet.descriptions");
        if (descriptions != null) {
            throw new IllegalStateException(descriptions);
        }
        String errors = System.getProperty("puppet.errors");
        if (errors != null) {
            throw new IllegalStateException(errors);
        }
        try {
            System.setProperty("puppet.descriptions", PuppetDoclet.descriptionsToString(optionDescriptions));
            System.setProperty("puppet.errors", String.valueOf(nInitErrors));
            javadoc(args.toArray(new String[0]));
        } finally {
            System.clearProperty("puppet.descriptions");
            System.clearProperty("puppet.errors");
        }

        long sumErrors = optionDescriptions.stream().mapToLong(d -> d.nProcessErrors).sum() + nInitErrors;
        boolean success = optionDescriptions.stream().allMatch(d -> d.success);

        checkOutput(Output.OUT, sumErrors != 0 || !success, "error - ");
    }

    /* Creating a specialized consumer is even more lightweight than creating a POJO */
    @FunctionalInterface
    public interface ScenarioConsumer {
        void consume(Path src, int nInitErrors, List<PuppetOption.Description> optionDescriptions);
    }

    public static final class PuppetDoclet implements Doclet {

        private final int nErrorsInInit;
        private final Set<PuppetOption.Description> descriptions;
        private Set<Option> options;
        private Reporter reporter;

        public PuppetDoclet() {
            this(nInitErrorsFromString(System.getProperty("puppet.errors")),
                 descriptionsFromString(System.getProperty("puppet.descriptions")));
        }

        private static Collection<PuppetOption.Description> descriptionsFromString(String value) {
            if (value.isEmpty())
                return List.of(); // special case of description of zero-length
            String[] split = value.split(",");
            List<PuppetOption.Description> descriptions = new ArrayList<>();
            for (int i = 0; i < split.length; i += 3) {
                String name = split[i];
                boolean success = Boolean.parseBoolean(split[i + 1]);
                int nErrors = Integer.parseInt(split[i + 2]);
                descriptions.add(new PuppetOption.Description(name, success, nErrors));
            }
            return descriptions;
        }

        public static String descriptionsToString(Collection<? extends PuppetOption.Description> descriptions) {
            return descriptions.stream()
                    .map(d -> d.name + "," + d.success + "," + d.nProcessErrors)
                    .collect(Collectors.joining(","));
        }

        private static int nInitErrorsFromString(String value) {
            return Integer.parseInt(Objects.requireNonNull(value));
        }

        public PuppetDoclet(int nErrorsInInit, Collection<PuppetOption.Description> descriptions) {
            if (nErrorsInInit < 0) {
                throw new IllegalArgumentException();
            }
            this.nErrorsInInit = nErrorsInInit;
            this.descriptions = Set.copyOf(descriptions);
        }

        @Override
        public void init(Locale locale, Reporter reporter) {
            this.reporter = reporter;
            for (int i = 0; i < nErrorsInInit; i++) {
                reporter.print(Diagnostic.Kind.ERROR, "init error #%s".formatted(i));
            }
        }

        @Override
        public String getName() {
            return getClass().getSimpleName();
        }

        @Override
        public Set<? extends Option> getSupportedOptions() {
            if (options == null) {
                options = new HashSet<>();
                for (PuppetOption.Description d : descriptions) {
                    options.add(new PuppetOption(d, reporter));
                }
            }
            return options;
        }

        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }

        @Override
        public boolean run(DocletEnvironment environment) {
            return true;
        }
    }

    private static final class PuppetOption implements Doclet.Option {

        private final Reporter reporter;
        private final Description description;

        public PuppetOption(Description description, Reporter reporter) {
            this.description = description;
            this.reporter = reporter;
        }

        @Override
        public int getArgumentCount() {
            return 0;
        }

        @Override
        public String getDescription() {
            return description.name
                    + ": success=" + description.success
                    + ", nProcessErrors=" + description.nProcessErrors;
        }

        @Override
        public Kind getKind() {
            return Kind.STANDARD;
        }

        @Override
        public List<String> getNames() {
            return List.of(description.name);
        }

        @Override
        public String getParameters() {
            return "";
        }

        @Override
        public boolean process(String option, List<String> arguments) {
            for (int i = 0; i < description.nProcessErrors; i++) {
                reporter.print(Diagnostic.Kind.ERROR,
                               "option: '%s', process error #%s".formatted(description.name, i));
            }
            return description.success;
        }

        public final static class Description {

            public final String name;
            public final boolean success;
            public final int nProcessErrors;

            Description(String name, boolean success, int nErrors) {
                this.name = name;
                this.success = success;
                this.nProcessErrors = nErrors;
            }

            @Override
            public String toString() {
                return "Description{" +
                        "name='" + name + '\'' +
                        ", success=" + success +
                        ", nProcessErrors=" + nProcessErrors +
                        '}';
            }
        }
    }
}