/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.xml.secure;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.transform.Source;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.vafer.jdependency.Clazz;
import org.vafer.jdependency.Clazzpath;

/**
 * Guards the shade footprint: the set of classes a consumer pulls in when they shade a single factory entry point.
 *
 * <p>Using {@code jdependency}, the same library {@code maven-shade-plugin}'s {@code minimizeJar} uses, this test computes each entry point's transitive class
 * closure over the compiled {@code target/classes} and pins it to an expected set. It keeps each entry point from silently regaining a dependency on classes it
 * should not need (for example a sibling resolver floor or another factory class), so schema builds only on the shared SAX path, TrAX and XPath additionally on the
 * DOM path their Xalan getAssociatedStylesheet and InputSource rewrites parse through, while the six public entry points together pull the whole
 * library. Update the expected sets deliberately: a change here is a change to what a downstream shade includes.</p>
 *
 * <p>The test reads the compiled {@code .class} files from the code-source location, which only exists on a regular JVM: a native image carries no bytecode (and
 * nobody shades one), so the test is disabled there, just as it is excluded from the Android test compile.</p>
 */
@DisabledInNativeImage
class ShadingFootprintTest {

    private static final String PKG = "org.apache.commons.xml.secure.";

    // @formatter:off
    private static final Set<String> DOCUMENT_BUILDER_FACTORY = set(
            "FallbackIgnoreEntityResolver2",
            "SecureDocumentBuilder",
            "SecureDocumentBuilderFactory",
            "SecureDocumentBuilderFactory$1",
            "SecureDocumentBuilderFactory$Wrapper",
            "SecureException",
            "MethodHandleFactory",
            "MethodHandleFactory$ThrowableCallable");
    // @formatter:on

    // @formatter:off
    private static final Set<String> SAX_PARSER_FACTORY = set(
            "FallbackIgnoreEntityResolver2",
            "SecureException",
            "SecureSAXParser",
            "SecureSAXParserFactory",
            "SecureSAXParserFactory$1",
            "SecureSAXParserFactory$SecureExpatXMLReader",
            "SecureSAXParserFactory$Wrapper",
            "SecureXMLReader",
            "MethodHandleFactory",
            "MethodHandleFactory$ThrowableCallable");
    // @formatter:on

    // @formatter:off
    private static final Set<String> XML_INPUT_FACTORY = set(
            "FallbackIgnoreXMLResolver",
            "SecureException",
            "SecureXMLInputFactory",
            "SecureXMLInputFactory$1",
            "SecureXMLInputFactory$Wrapper",
            "MethodHandleFactory",
            "MethodHandleFactory$ThrowableCallable");
    // @formatter:on

    /**
     * TrAX, XPath and schema re-harden their sub-parsers through {@link SecureSAXParserFactory#secure(Source, boolean)}, so each builds on the full SAX closure below;
     * TrAX additionally parses the Xalan {@code getAssociatedStylesheet} source and XPath its InputSource-taking evaluate calls through the DOM entry point, so
     * their closures carry that set too.
     */
    // @formatter:off
    private static final Set<String> TRANSFORMER_FACTORY = saxParserFactoryPlus(
            "FallbackIgnoreEntityResolver2",
            "FallbackIgnoreURIResolver",
            "SecureDocumentBuilder",
            "SecureDocumentBuilderFactory",
            "SecureDocumentBuilderFactory$1",
            "SecureDocumentBuilderFactory$Wrapper",
            "SecureException",
            "SecureSAXParser",
            "SecureSAXParserFactory",
            "SecureSAXParserFactory$1",
            "SecureSAXParserFactory$SecureExpatXMLReader",
            "SecureSAXParserFactory$Wrapper",
            "SecureTemplates",
            "SecureTemplatesHandler",
            "SecureTransformer",
            "SecureTransformerFactory",
            "SecureTransformerFactory$1",
            "SecureTransformerFactory$Wrapper",
            "SecureTransformerHandler",
            "SecureXMLFilter",
            "SecureXMLReader",
            "SaxonProvider",
            "SaxonProvider$1",
            "SaxonProvider$SecureConfiguration",
            "SaxonProvider$SaxonProviderConfigurer");
    // @formatter:on

    // @formatter:off
    private static final Set<String> XPATH_FACTORY = saxParserFactoryPlus(
            "FallbackIgnoreEntityResolver2",
            "FallbackIgnoreURIResolver",
            "SecureDocumentBuilder",
            "SecureDocumentBuilderFactory",
            "SecureDocumentBuilderFactory$1",
            "SecureDocumentBuilderFactory$Wrapper",
            "MethodHandleFactory",
            "MethodHandleFactory$ThrowableCallable",
            "SecureException",
            "SecureSAXParser",
            "SecureSAXParserFactory",
            "SecureSAXParserFactory$1",
            "SecureSAXParserFactory$SecureExpatXMLReader",
            "SecureSAXParserFactory$Wrapper",
            "SecureXMLReader",
            "SecureXPath",
            "SecureXPathExpression",
            "SecureXPathFactory",
            "SecureXPathFactory$1",
            "SecureXPathFactory$Wrapper",
            "SaxonProvider",
            "SaxonProvider$1",
            "SaxonProvider$SecureConfiguration",
            "SaxonProvider$SaxonProviderConfigurer");
    // @formatter:on

    // @formatter:off
    private static final Set<String> SCHEMA_FACTORY = saxParserFactoryPlus(
            "FallbackIgnoreEntityResolver2",
            "FallbackIgnoreLSResourceResolver",
            "SecureException",
            "SecureSAXParser",
            "SecureSAXParserFactory",
            "SecureSAXParserFactory$1",
            "SecureSAXParserFactory$SecureExpatXMLReader",
            "SecureSAXParserFactory$Wrapper",
            "SecureSchema",
            "SecureSchemaFactory",
            "SecureSchemaFactory$1",
            "SecureSchemaFactory$Wrapper",
            "SecureValidator",
            "SecureValidatorHandler",
            "SecureXMLReader");
    // @formatter:on

    /**
     * Class count of the {@link #rootClosure()} DOM entry point, guarding that closure against drift.
     */
    private static final int LIBRARY_CLASS_COUNT = 8;

    /**
     * Entry points reported by the {@link #reportFootprint()} diagnostic, most-focused first, ending with the whole library.
     */
    private static final String[] REPORTED = {"SecureDocumentBuilderFactory", "SecureSAXParserFactory", "SecureXMLInputFactory",
            "SecureTransformerFactory", "SecureXPathFactory", "SecureSchemaFactory"};

    private static Clazzpath clazzpath;
    private static Path classesDir;

    /**
     * Sums the uncompressed {@code .class} file sizes of a closure's classes, as they would land in a shaded jar.
     */
    private static long bytesOf(final Set<String> simpleNames) {
        long total = 0;
        for (final String name : simpleNames) {
            try {
                total += Files.size(classesDir.resolve("org/apache/commons/xml/secure/" + name + ".class"));
            } catch (final IOException e) {
                throw new UncheckedIOException(e);
            }
        }
        return total;
    }

    /**
     * Transitive class closure of {@code PKG + simpleName}, restricted to this library's own package and reported by simple name.
     */
    private static Set<String> closureOf(final String simpleName) {
        final Clazz entry = clazzpath.getClazz(PKG + simpleName);
        if (entry == null) {
            throw new IllegalStateException("Not on the compiled classpath: " + PKG + simpleName);
        }
        final Set<String> names = new TreeSet<>();
        names.add(strip(entry.getName()));
        for (final Clazz dependency : entry.getTransitiveDependencies()) {
            if (dependency.getName().startsWith(PKG)) {
                names.add(strip(dependency.getName()));
            }
        }
        return names;
    }

    @BeforeAll
    static void indexCompiledClasses() throws Exception {
        classesDir = Paths.get(SecureException.class.getProtectionDomain().getCodeSource().getLocation().toURI());
        clazzpath = new Clazzpath();
        clazzpath.addClazzpathUnit(classesDir);
    }

    /**
     * Prints each entry point's shade closure size (uncompressed {@code .class} bytes) and its share of the full library, to track the footprint over the
     * refactor.
     */
    @AfterAll
    static void reportFootprint() {
        final Set<String> libraryClosure = new TreeSet<>();
        for (final String entry : REPORTED) {
            libraryClosure.addAll(closureOf(entry));
        }
        final long library = bytesOf(libraryClosure);
        final StringBuilder report = new StringBuilder("\nShade footprint (uncompressed .class bytes, % of full library):\n");
        for (final String entry : REPORTED) {
            final Set<String> closure = closureOf(entry);
            final long bytes = bytesOf(closure);
            report.append(String.format(Locale.ROOT, "  %-24s %2d classes  %7d bytes  %5.1f%%%n", entry, closure.size(), bytes, 100.0 * bytes / library));
        }
        if (Boolean.getBoolean(ShadingFootprintTest.class.getName() + ".reportFootprint")) {
            System.out.print(report);
        }
    }

    private static Set<String> rootClosure() {
        return closureOf("SecureDocumentBuilderFactory");
    }

    /**
     * {@link #SAX_PARSER_FACTORY} plus the extra names; used where an entry point's closure is the SAX path plus its own classes.
     */
    private static Set<String> saxParserFactoryPlus(final String... more) {
        final Set<String> union = new TreeSet<>(SAX_PARSER_FACTORY);
        union.addAll(Arrays.asList(more));
        return union;
    }

    private static Set<String> set(final String... names) {
        return new TreeSet<>(Arrays.asList(names));
    }

    private static String strip(final String qualifiedName) {
        return qualifiedName.substring(PKG.length());
    }

    @Test
    void documentBuilderFactoryFootprint() {
        assertEquals(DOCUMENT_BUILDER_FACTORY, closureOf("SecureDocumentBuilderFactory"));
    }

    @Test
    void rootClosureMatchesDocumentBuilderFootprint() {
        assertEquals(LIBRARY_CLASS_COUNT, rootClosure().size(), "SecureDocumentBuilderFactory closure size drifted");
    }

    @Test
    void saxParserFactoryFootprint() {
        assertEquals(SAX_PARSER_FACTORY, closureOf("SecureSAXParserFactory"));
    }

    @Test
    void schemaFactoryFootprint() {
        assertEquals(SCHEMA_FACTORY, closureOf("SecureSchemaFactory"));
    }

    @Test
    void transformerFactoryFootprint() {
        assertEquals(TRANSFORMER_FACTORY, closureOf("SecureTransformerFactory"));
    }

    @Test
    void xmlInputFactoryFootprint() {
        assertEquals(XML_INPUT_FACTORY, closureOf("SecureXMLInputFactory"));
    }

    @Test
    void xPathFactoryFootprint() {
        assertEquals(XPATH_FACTORY, closureOf("SecureXPathFactory"));
    }
}
