/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This 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 software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.subsystem.test;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILURE_DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REMOVE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;

import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.jboss.as.controller.AttributeDefinition;
import org.jboss.as.controller.CapabilityReferenceRecorder;
import org.jboss.as.controller.CompositeOperationHandler;
import org.jboss.as.controller.Extension;
import org.jboss.as.controller.ModelVersion;
import org.jboss.as.controller.NotificationDefinition;
import org.jboss.as.controller.OperationDefinition;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.ProcessType;
import org.jboss.as.controller.ProxyController;
import org.jboss.as.controller.ResourceDefinition;
import org.jboss.as.controller.RunningMode;
import org.jboss.as.controller.RunningModeControl;
import org.jboss.as.controller.access.management.AccessConstraintDefinition;
import org.jboss.as.controller.capability.RuntimeCapability;
import org.jboss.as.controller.client.helpers.Operations;
import org.jboss.as.controller.descriptions.DescriptionProvider;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.descriptions.OverrideDescriptionProvider;
import org.jboss.as.controller.extension.ExtensionRegistry;
import org.jboss.as.controller.extension.ExtensionRegistryType;
import org.jboss.as.controller.extension.RuntimeHostControllerInfoAccessor;
import org.jboss.as.controller.extension.SubsystemInformation;
import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.controller.parsing.ExtensionParsingContext;
import org.jboss.as.controller.persistence.ConfigurationPersister;
import org.jboss.as.controller.registry.AliasEntry;
import org.jboss.as.controller.registry.AttributeAccess;
import org.jboss.as.controller.registry.ImmutableManagementResourceRegistration;
import org.jboss.as.controller.registry.ManagementResourceRegistration;
import org.jboss.as.controller.registry.NotificationEntry;
import org.jboss.as.controller.registry.OperationEntry;
import org.jboss.as.controller.registry.OperationEntry.Flag;
import org.jboss.as.controller.registry.Resource;
import org.jboss.as.controller.registry.RuntimePackageDependency;
import org.jboss.as.controller.transform.ExtensionTransformerRegistration;
import org.jboss.as.controller.transform.OperationTransformer.TransformedOperation;
import org.jboss.as.model.test.ChildFirstClassLoaderBuilder;
import org.jboss.as.model.test.EAPRepositoryReachableUtil;
import org.jboss.as.model.test.ModelFixer;
import org.jboss.as.model.test.ModelTestBootOperationsBuilder;
import org.jboss.as.model.test.ModelTestControllerVersion;
import org.jboss.as.model.test.ModelTestModelControllerService;
import org.jboss.as.model.test.ModelTestOperationValidatorFilter;
import org.jboss.as.model.test.ModelTestOperationValidatorFilter.Action;
import org.jboss.as.model.test.ModelTestParser;
import org.jboss.as.model.test.ModelTestUtils;
import org.jboss.as.model.test.OperationFixer;
import org.jboss.as.model.test.StringConfigurationPersister;
import org.jboss.as.subsystem.bridge.impl.LegacyControllerKernelServicesProxy;
import org.jboss.as.subsystem.bridge.local.ScopedKernelServicesBootstrap;
import org.jboss.as.subsystem.test.ModelDescriptionValidator.ValidationConfiguration;
import org.jboss.dmr.ModelNode;
import org.jboss.modules.filter.ClassFilter;
import org.jboss.staxmapper.XMLElementReader;
import org.jboss.staxmapper.XMLMapper;
import org.junit.Assert;
import org.junit.Assume;
import org.wildfly.legacy.test.spi.Version;

/**
 *
 * @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
 */
final class SubsystemTestDelegate {

    private static final String TEST_OLD_LEGACY = "jboss.test.transformers.subsystem.old";
    private static final String TEST_NAMESPACE = "urn.org.jboss.test:1.0";

    private static final ModelNode SUCCESS;
    static{
        SUCCESS = new ModelNode();
        SUCCESS.get(ModelDescriptionConstants.OUTCOME).set(ModelDescriptionConstants.SUCCESS);
        SUCCESS.get(ModelDescriptionConstants.RESULT);
        SUCCESS.protect();
    }

    private final Class<?> testClass;
    private final List<KernelServices> kernelServices = new ArrayList<>();

    protected final String mainSubsystemName;
    private final Extension mainExtension;
    private final Comparator<PathAddress> removeOrderComparator;

    /**
     * ExtensionRegistry we use just for registering parsers.
     * The ModelControllerService uses a separate registry. This is done this way to allow multiple ModelControllerService
     * instantiations in the same test without having to re-initialize the parsers.
     */
    private ExtensionRegistry extensionParsingRegistry;
    private ModelTestParser testParser;
    private boolean addedExtraParsers;
    private XMLMapper xmlMapper;

    /**
     * Creates a new delegate.
     *
     * @param testClass             the test class
     * @param mainSubsystemName     the name of the subsystem
     * @param mainExtension         the extension to test
     * @param removeOrderComparator a comparator to sort addresses when removing the subsystem, {@code null} if order
     *                              doesn't matter
     */
    SubsystemTestDelegate(final Class<?> testClass, final String mainSubsystemName, final Extension mainExtension, final Comparator<PathAddress> removeOrderComparator) {
        this.testClass = testClass;
        this.mainSubsystemName = mainSubsystemName;
        this.mainExtension = mainExtension;
        this.removeOrderComparator = removeOrderComparator;
    }

    String getMainSubsystemName() {
        return mainSubsystemName;
    }

    void initializeParser() throws Exception {
        //Initialize the parser
        xmlMapper = XMLMapper.Factory.create();
        extensionParsingRegistry = new ExtensionRegistry(getProcessType(), new RunningModeControl(RunningMode.NORMAL), null, null, null, RuntimeHostControllerInfoAccessor.SERVER);
        testParser = new TestParser(mainSubsystemName, extensionParsingRegistry);
        xmlMapper.registerRootElement(new QName(TEST_NAMESPACE, "test"), testParser);
        mainExtension.initializeParsers(extensionParsingRegistry.getExtensionParsingContext("Test", xmlMapper));
        addedExtraParsers = false;
    }

    void cleanup() throws Exception {
        for (KernelServices kernelServices : this.kernelServices) {
            try {
                kernelServices.shutdown();
            } catch (Exception e) {
                //we don't care
            }
        }
        kernelServices.clear();
        xmlMapper = null;
        extensionParsingRegistry = null;
        testParser = null;
    }

    Extension getMainExtension() {
        return mainExtension;
    }

    /**
     * Parse the subsystem xml and create the operations that will be passed into the controller
     *
     * @param subsystemXml the subsystem xml to be parsed
     * @return the created operations
     * @throws XMLStreamException if there is a parsing problem
     */
    List<ModelNode> parse(String subsystemXml) throws XMLStreamException {
        return parse(null, subsystemXml);
    }

    /**
     * Parse the subsystem xml and create the operations that will be passed into the controller
     *
     * @param additionalParsers additional initialization that should be done to the parsers before initializing our extension. These parsers
     *                          will only be initialized the first time this method is called from within a test
     * @param subsystemXml      the subsystem xml to be parsed
     * @return the created operations
     * @throws XMLStreamException if there is a parsing problem
     */
    List<ModelNode> parse(AdditionalParsers additionalParsers, String subsystemXml) throws XMLStreamException {
        String xml = "<test xmlns=\"" + TEST_NAMESPACE + "\">" +
                subsystemXml +
                "</test>";
        final XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(new StringReader(xml));
        addAdditionalParsers(additionalParsers);
        final List<ModelNode> operationList = new ArrayList<>();
        xmlMapper.parseDocument(operationList, reader);
        return operationList;
    }

    /**
     * Output the model to xml
     *
     * @param model the model to marshall
     * @return the xml
     */
    String outputModel(ModelNode model) throws Exception {

        StringConfigurationPersister persister = new StringConfigurationPersister(Collections.emptyList(), testParser, true);

        // Use ProcessType.HOST_CONTROLLER for this ExtensionRegistry so we don't need to provide
        // a PathManager via the ExtensionContext. All we need the Extension to do here is register the xml writers
        ExtensionRegistry outputExtensionRegistry = new ExtensionRegistry(ProcessType.HOST_CONTROLLER, new RunningModeControl(RunningMode.NORMAL), null, null, null, RuntimeHostControllerInfoAccessor.SERVER);
        outputExtensionRegistry.setWriterRegistry(persister);


        Extension extension = mainExtension.getClass().newInstance();
        extension.initialize(outputExtensionRegistry.getExtensionContext("Test", MOCK_RESOURCE_REG, ExtensionRegistryType.SLAVE));

        ConfigurationPersister.PersistenceResource resource = persister.store(model, Collections.emptySet());
        resource.commit();
        return persister.getMarshalled();
    }

    /**
     * Creates a new kernel services builder used to create a new controller containing the subsystem being tested
     *
     * @param additionalInit Additional initialization that should be done to the parsers, controller and service container before initializing our extension
     */
    KernelServicesBuilder createKernelServicesBuilder(AdditionalInitialization additionalInit) {
        return new KernelServicesBuilderImpl(additionalInit);
    }

    /**
     * Gets the ProcessType to use when initializing the parsers. Defaults to {@link ProcessType#EMBEDDED_SERVER}
     * To tweak the process type when installing a controller, override {@link AdditionalInitialization} and pass in to
     * {@link #createKernelServicesBuilder(AdditionalInitialization)} instead.
     *
     * @return the process type
     */
    ProcessType getProcessType() {
        return ProcessType.EMBEDDED_SERVER;
    }

    /**
     * Checks that the subystem resources can be removed, i.e. that people have registered
     * working 'remove' operations for every 'add' level.
     *
     * @param kernelServices the kernel services used to access the controller
     */
    void assertRemoveSubsystemResources(KernelServices kernelServices) {
        assertRemoveSubsystemResources(kernelServices, null);
    }

    /**
     * Checks that the subystem resources can be removed, i.e. that people have registered
     * working 'remove' operations for every 'add' level.
     *
     * @param kernelServices        the kernel services used to access the controller
     * @param ignoredChildAddresses child addresses that should not be removed, they are managed by one of the parent resources.
     *                              This set cannot contain the subsystem resource itself
     */
    void assertRemoveSubsystemResources(KernelServices kernelServices, Set<PathAddress> ignoredChildAddresses) {

        if (ignoredChildAddresses == null) {
            ignoredChildAddresses = Collections.emptySet();
        } else {
            PathAddress subsystem = PathAddress.pathAddress(PathElement.pathElement(SUBSYSTEM, mainSubsystemName));
            Assert.assertFalse("Cannot exclude removal of subsystem itself", ignoredChildAddresses.contains(subsystem));
        }

        Resource rootResource = ModelTestModelControllerService.grabRootResource(kernelServices);

        List<PathAddress> addresses = new ArrayList<>();
        PathAddress pathAddress = PathAddress.pathAddress(PathElement.pathElement(SUBSYSTEM, mainSubsystemName));
        Resource subsystemResource = rootResource.getChild(pathAddress.getLastElement());
        Assert.assertNotNull(subsystemResource);
        addresses.add(pathAddress);

        getAllChildAddressesForRemove(pathAddress, addresses, subsystemResource);

        // Remove ignored children
        removeIgnoredChildren(ignoredChildAddresses, addresses);

        // If the remove order comparator is not null, then sort the addresses
        if (removeOrderComparator != null) {
            addresses.sort(removeOrderComparator);
        }

        ModelNode composite = new ModelNode();
        composite.get(OP).set(CompositeOperationHandler.NAME);
        composite.get(OP_ADDR).setEmptyList();
        composite.get("rollback-on-runtime-failure").set(true);


        for (ListIterator<PathAddress> iterator = addresses.listIterator(addresses.size()); iterator.hasPrevious(); ) {
            PathAddress cur = iterator.previous();
            ModelNode remove = new ModelNode();
            remove.get(OP).set(REMOVE);
            remove.get(OP_ADDR).set(cur.toModelNode());
            composite.get("steps").add(remove);
        }


        final ModelNode result = kernelServices.executeOperation(composite);
        Assert.assertTrue(result.get(FAILURE_DESCRIPTION).asString(), Operations.isSuccessfulOutcome(result));

        ModelNode model = kernelServices.readWholeModel().get(SUBSYSTEM, mainSubsystemName);
        Assert.assertFalse("Subsystem resources were not removed " + model, model.isDefined());
    }

    private void getAllChildAddressesForRemove(PathAddress address, List<PathAddress> addresses, Resource resource) {
        List<PathElement> childElements = new ArrayList<>();
        for (String type : resource.getChildTypes()) {
            for (String childName : resource.getChildrenNames(type)) {
                PathElement element = PathElement.pathElement(type, childName);
                childElements.add(element);
            }
        }

        for (PathElement childElement : childElements) {
            // Ignore runtime resources
            if (!resource.getChild(childElement).isRuntime()) {
                addresses.add(address.append(childElement));
            }
        }

        for (PathElement childElement : childElements) {
            // Ignore runtime resources
            final Resource childResource = resource.getChild(childElement);
            if (!childResource.isRuntime()) {
                getAllChildAddressesForRemove(address.append(childElement), addresses, childResource);
            }
        }
    }

    private void removeIgnoredChildren(final Collection<PathAddress> ignoredChildAddresses, final Collection<PathAddress> addresses) {
        // Remove all known ignored children
        addresses.removeAll(ignoredChildAddresses);
        // Checked for wildcards removals
        for (PathAddress ignoredChildAddress : ignoredChildAddresses) {
            final PathElement lastIgnoredElement = ignoredChildAddress.getLastElement();
            if (lastIgnoredElement.isWildcard()) {
                // Check each address
                for (final Iterator<PathAddress> iterator = addresses.iterator(); iterator.hasNext(); ) {
                    final PathAddress childAddress = iterator.next();
                    if (childAddress.size() == ignoredChildAddress.size()) {
                        // Check the last element key for a match
                        if (lastIgnoredElement.getKey().equals(childAddress.getLastElement().getKey())) {
                            boolean match = true;
                            // Check for matches on previous elements
                            for (int i = 0; i < ignoredChildAddress.size() - 1; i++) {
                                final PathElement e1 = ignoredChildAddress.getElement(i);
                                final PathElement e2 = childAddress.getElement(i);
                                if (!e1.equals(e2)) {
                                    match = false;
                                    break;
                                }
                            }
                            if (match) {
                                iterator.remove();
                            }
                        }
                    }
                }
            }
        }
    }



    /**
     * Checks that the transformed model is the same as the model built up in the legacy subsystem controller via the transformed operations,
     * and that the transformed model is valid according to the resource definition in the legacy subsystem controller.
     *
     *
     *
     * @param kernelServices the main kernel services
     * @param modelVersion   the model version of the targeted legacy subsystem
     * @param legacyModelFixer use to touch up the model read from the legacy controller, use sparingly when the legacy model is just wrong. May be {@code null}
     * @param includeDefaults  whether the legacy controller model and the transformed model should include default values for undefined attributes
     * @return the whole model of the legacy controller
     */
    ModelNode checkSubsystemModelTransformation(KernelServices kernelServices, ModelVersion modelVersion, ModelFixer legacyModelFixer, boolean includeDefaults) throws IOException, OperationFailedException {

        ModelNode legacyReadResource = Util.createOperation(ModelDescriptionConstants.READ_RESOURCE_OPERATION, PathAddress.EMPTY_ADDRESS);
        legacyReadResource.get(ModelDescriptionConstants.RECURSIVE).set(true);
        legacyReadResource.get(ModelDescriptionConstants.INCLUDE_ALIASES).set(false);
        legacyReadResource.get(ModelDescriptionConstants.INCLUDE_RUNTIME).set(false);
        legacyReadResource.get(ModelDescriptionConstants.INCLUDE_DEFAULTS).set(includeDefaults);
        ModelNode legacyModel = ModelTestUtils.checkResultAndGetContents(kernelServices.executeOperation(modelVersion, kernelServices.transformOperation(modelVersion, legacyReadResource)));
        ModelNode legacySubsystem = legacyModel.require(SUBSYSTEM);
        legacySubsystem = legacySubsystem.require(mainSubsystemName);

        if (legacyModelFixer != null) {
            legacySubsystem = legacyModelFixer.fixModel(legacySubsystem);
        }

        //1) Check that the transformed model is the same as the whole model read from the legacy controller.
        //The transformed model is done via the resource transformers
        //The model in the legacy controller is built up via transformed operations
        ModelNode transformed = kernelServices.readTransformedModel(modelVersion, includeDefaults).get(SUBSYSTEM, mainSubsystemName);

        ModelTestUtils.compare(legacySubsystem, transformed, true);

        //2) Check that the transformed model is valid according to the resource definition in the legacy subsystem controller
        ResourceDefinition rd = TransformationUtils.getResourceDefinition(kernelServices, modelVersion, mainSubsystemName);
        Assert.assertNotNull("Could not load legacy dmr for subsystem '" + mainSubsystemName + "' version: '" + modelVersion + "' please add it", rd);
        ManagementResourceRegistration rr = ManagementResourceRegistration.Factory.forProcessType(getProcessType()).createRegistration(rd);
        ModelTestUtils.checkModelAgainstDefinition(transformed, rr);
        return legacyModel;
    }

    void addAdditionalParsers(AdditionalParsers additionalParsers) {
        if (additionalParsers != null && !addedExtraParsers) {
            additionalParsers.addParsers(extensionParsingRegistry, xmlMapper);
            addedExtraParsers = true;
        }
    }

    private ExtensionRegistry cloneExtensionRegistry(AdditionalInitialization additionalInit) {
        final ExtensionRegistry clone = new ExtensionRegistry(additionalInit.getProcessType(), new RunningModeControl(additionalInit.getExtensionRegistryRunningMode()), null, null, null, RuntimeHostControllerInfoAccessor.SERVER);
        for (String extension : extensionParsingRegistry.getExtensionModuleNames()) {
            ExtensionParsingContext epc = clone.getExtensionParsingContext(extension, null);
            for (Map.Entry<String, SubsystemInformation> entry : extensionParsingRegistry.getAvailableSubsystems(extension).entrySet()) {
                for (String namespace : entry.getValue().getXMLNamespaces()) {
                    epc.setSubsystemXmlMapping(entry.getKey(), namespace, (XMLElementReader) null);
                }
            }
        }

        return clone;
    }

    private void validateDescriptionProviders(AdditionalInitialization additionalInit, KernelServices kernelServices) {
        ValidationConfiguration arbitraryDescriptors = additionalInit.getModelValidationConfiguration();
        ModelNode address = new ModelNode();
        address.setEmptyList();
        address.add("subsystem", mainSubsystemName);

        ModelNode op = new ModelNode();
        op.get(OP).set("read-resource-description");
        op.get(OP_ADDR).set(address);
        op.get("recursive").set(true);
        op.get("inherited").set(false);
        op.get("operations").set(true);
        op.get("notifications").set(true);
        op.get("include-aliases").set(true);
        ModelNode result = kernelServices.executeOperation(op);
        if (result.hasDefined(FAILURE_DESCRIPTION)) {
            throw new RuntimeException(result.get(FAILURE_DESCRIPTION).toString());
        }
        ModelNode model = result.get(RESULT);

        //System.out.println(model);

        ModelDescriptionValidator validator = new ModelDescriptionValidator(address, model, arbitraryDescriptors);
        List<ModelDescriptionValidator.ValidationFailure> validationMessages = validator.validateResource();
        if (validationMessages.size() > 0) {
            final StringBuilder builder = new StringBuilder("VALIDATION ERRORS IN MODEL:");
            for (ModelDescriptionValidator.ValidationFailure failure : validationMessages) {
                builder.append(failure);
                builder.append("\n");

            }
            if (arbitraryDescriptors != null) {
                Assert.fail("Failed due to validation errors in the model. Please fix :-) " + builder.toString());
            }
        }
    }


    private class KernelServicesBuilderImpl implements KernelServicesBuilder, ModelTestBootOperationsBuilder.BootOperationParser {
        private final ModelTestBootOperationsBuilder bootOperationBuilder;
        private final AdditionalInitialization additionalInit;
        private Map<ModelVersion, LegacyKernelServiceInitializerImpl> legacyControllerInitializers = new HashMap<ModelVersion, LegacyKernelServiceInitializerImpl>();

        public KernelServicesBuilderImpl(AdditionalInitialization additionalInit) {
            this.additionalInit = additionalInit == null ? new AdditionalInitialization() : additionalInit;
            bootOperationBuilder = new ModelTestBootOperationsBuilder(testClass, this);
        }

        @Override
        public KernelServicesBuilder setSubsystemXmlResource(String resource) throws IOException, XMLStreamException {
            bootOperationBuilder.setXmlResource(resource);
            return this;
        }

        @Override
        public KernelServicesBuilder setSubsystemXml(String subsystemXml) throws XMLStreamException {
            bootOperationBuilder.setXml(subsystemXml);
            return this;
        }

        public KernelServicesBuilder setBootOperations(List<ModelNode> bootOperations) {
            bootOperationBuilder.setBootOperations(bootOperations);
            return this;
        }

        @Override
        public KernelServicesBuilder setBootOperations(ModelNode... bootOperations) {
            bootOperationBuilder.setBootOperations(Arrays.asList(bootOperations));
            return this;
        }

        public LegacyKernelServicesInitializer createLegacyKernelServicesBuilder(AdditionalInitialization additionalInit, ModelTestControllerVersion version, ModelVersion modelVersion) {
            boolean valid = version.hasValidLegacyController();
            if (!valid) {
                valid = System.getProperties().containsKey(TEST_OLD_LEGACY);
            }
            Assume.assumeTrue("No legacy controller to test against", valid);
            //Ignore this test if it is eap
            if (version.isEap()) {
                Assume.assumeTrue(EAPRepositoryReachableUtil.isReachable());
            }

            bootOperationBuilder.validateNotAlreadyBuilt();
            if (legacyControllerInitializers.containsKey(modelVersion)) {
                throw new IllegalArgumentException("There is already a legacy controller for " + modelVersion);
            }
            if (additionalInit != null) {
                if (additionalInit.getRunningMode() != RunningMode.ADMIN_ONLY) {
                    throw new IllegalArgumentException("The additional initialization must have a running mode of ADMIN_ONLY, it was " + additionalInit.getRunningMode());
                }
            }

            LegacyKernelServiceInitializerImpl initializer = new LegacyKernelServiceInitializerImpl(additionalInit, version, modelVersion);
            legacyControllerInitializers.put(modelVersion, initializer);
            return initializer;
        }

        public LegacyKernelServicesInitializer createLegacyKernelServicesBuilder(AdditionalInitialization additionalInit, ModelTestControllerVersion version) {


            return createLegacyKernelServicesBuilder(additionalInit, version, version.getSubsystemModelVersion(mainSubsystemName));
        }

        public KernelServices build() throws Exception {
            bootOperationBuilder.validateNotAlreadyBuilt();
            List<ModelNode> bootOperations = bootOperationBuilder.build();
            AbstractKernelServicesImpl kernelServices = AbstractKernelServicesImpl.create(testClass, mainSubsystemName, additionalInit, ModelTestOperationValidatorFilter.createValidateAll(), cloneExtensionRegistry(additionalInit), bootOperations,
                    testParser, mainExtension, null, legacyControllerInitializers.size() > 0, true);
            SubsystemTestDelegate.this.kernelServices.add(kernelServices);
            validateDescriptionProviders(additionalInit, kernelServices);
            ImmutableManagementResourceRegistration subsystemReg = kernelServices.getRootRegistration().getSubModel(PathAddress.pathAddress(PathElement.pathElement(SUBSYSTEM, mainSubsystemName)));
            ModelTestUtils.validateModelDescriptions(PathAddress.EMPTY_ADDRESS, subsystemReg);

            if (!legacyControllerInitializers.isEmpty()) { //only load transformers if we are testing legacy controllers
                loadTransformers(kernelServices.getExtensionRegistry());
            }

            for (Map.Entry<ModelVersion, LegacyKernelServiceInitializerImpl> entry : legacyControllerInitializers.entrySet()) {
                LegacyKernelServiceInitializerImpl legacyInitializer = entry.getValue();

                List<ModelNode> transformedBootOperations = new ArrayList<ModelNode>();
                for (ModelNode op : bootOperations) {

                    TransformedOperation transformedOp = kernelServices.transformOperation(entry.getKey(), op);
                    if (transformedOp.getTransformedOperation() != null) {
                        //Since rejected operations now execute on the slave to determine if it has been ignored or not
                        //we check the reject policy simulating a reject before adding it to the list of boot operations
                        if (!transformedOp.rejectOperation(SUCCESS)) {
                            transformedBootOperations.add(transformedOp.getTransformedOperation());
//                        } else {
//                            System.out.println(transformedOp.getFailureDescription());
                        }
                    }
                }

                LegacyControllerKernelServicesProxy legacyServices = legacyInitializer.install(kernelServices, transformedBootOperations);
                kernelServices.addLegacyKernelService(entry.getKey(), legacyServices);
            }

            return kernelServices;
        }

        private void loadTransformers(final ExtensionRegistry extensionRegistry) {
            ModelVersion version = extensionRegistry.getSubsystemInfo(getMainSubsystemName()).getManagementInterfaceVersion();
            for (ExtensionTransformerRegistration registration : ServiceLoader.load(ExtensionTransformerRegistration.class)) {
                if (registration.getSubsystemName().equals(getMainSubsystemName())) {
                    registration.registerTransformers(extensionRegistry.getTransformerRegistry()
                            .createSubsystemTransformerRegistration(getMainSubsystemName(), version));
                }

            }
        }


        @Override
        public List<ModelNode> parse(String subsystemXml) throws XMLStreamException {
            return SubsystemTestDelegate.this.parse(additionalInit, subsystemXml);
        }

        @Override
        public List<ModelNode> parseXml(String xml) throws Exception {
            ModelTestBootOperationsBuilder builder = new ModelTestBootOperationsBuilder(testClass, this);
            builder.setXml(xml);
            return builder.build();
        }

        @Override
        public List<ModelNode> parseXmlResource(String xmlResource) throws Exception {
            ModelTestBootOperationsBuilder builder = new ModelTestBootOperationsBuilder(testClass, this);
            builder.setXmlResource(xmlResource);
            return builder.build();
        }

        @Override
        public KernelServicesBuilder enableTransformerAttachmentGrabber() {
            return this;
        }

    }

    private class LegacyKernelServiceInitializerImpl implements LegacyKernelServicesInitializer {

        private final AdditionalInitialization additionalInit;
        private final ModelTestControllerVersion testControllerVersion;
        private String extensionClassName;
        private ModelVersion modelVersion;
        private ChildFirstClassLoaderBuilder classLoaderBuilder;
        private ModelTestOperationValidatorFilter.Builder operationValidationExcludeBuilder;
        private boolean persistXml = true;
        private boolean skipReverseCheck;
        private AdditionalInitialization reverseCheckConfig = AdditionalInitialization.MANAGEMENT;
        private ModelFixer reverseCheckModelFixer;
        private OperationFixer reverseCheckOperationFixer = operation -> operation;

        public LegacyKernelServiceInitializerImpl(AdditionalInitialization additionalInit, ModelTestControllerVersion version, ModelVersion modelVersion) {
            this.classLoaderBuilder = new ChildFirstClassLoaderBuilder(version.isEap());
            this.additionalInit = additionalInit == null ? AdditionalInitialization.MANAGEMENT : additionalInit;
            this.testControllerVersion = version;
            this.modelVersion = modelVersion;
        }


        @Override
        public LegacyKernelServicesInitializer addOperationValidationExclude(String name, PathAddress pathAddress) {
            addOperationValidationConfig(name, pathAddress, Action.NOCHECK, null);
            return this;
        }


        @Override
        public LegacyKernelServicesInitializer addOperationValidationResolve(String name, PathAddress pathAddress) {
            addOperationValidationConfig(name, pathAddress, Action.RESOLVE, null);
            return this;
        }

        @Override
        public LegacyKernelServicesInitializer addOperationValidationFixer(String name, PathAddress pathAddress, OperationFixer operationFixer) {
            addOperationValidationConfig(name, pathAddress, null, operationFixer);
            return this;
        }


        private void addOperationValidationConfig(String name, PathAddress pathAddress, Action action, OperationFixer operationFixer) {
            if (!additionalInit.isValidateOperations()) {
                throw new IllegalStateException("The additional initialization used to create this builder has turned off operation validation. That is not compatible with calling this method");
            }
            if (operationValidationExcludeBuilder == null) {
                operationValidationExcludeBuilder = ModelTestOperationValidatorFilter.createBuilder();
            }
            operationValidationExcludeBuilder.addOperation(pathAddress, name, action, operationFixer);
        }

        @Override
        public LegacyKernelServicesInitializer setExtensionClassName(String extensionClassName) {
            this.extensionClassName = extensionClassName;
            return this;
        }


        @Override
        public LegacyKernelServicesInitializer addURL(URL url) {
            classLoaderBuilder.addURL(url);
            return this;
        }

        @Override
        public LegacyKernelServicesInitializer addSimpleResourceURL(String resource) throws MalformedURLException {
            classLoaderBuilder.addSimpleResourceURL(resource);
            return this;
        }

        @Override
        public LegacyKernelServicesInitializer addMavenResourceURL(String...artifactGavs) throws IOException, ClassNotFoundException {
            for (String artifactGav : artifactGavs) {
                classLoaderBuilder.addMavenResourceURL(artifactGav);
            }
            return this;
        }

        @Override
        public LegacyKernelServiceInitializerImpl addParentFirstClassPattern(String pattern) {
            classLoaderBuilder.addParentFirstClassPattern(pattern);
            return this;
        }

        @Override
        public LegacyKernelServiceInitializerImpl addChildFirstClassPattern(String pattern) {
            classLoaderBuilder.addChildFirstClassPattern(pattern);
            return this;
        }

        @Override
        public LegacyKernelServicesInitializer excludeFromParent(ClassFilter exclusionFilter) {
            classLoaderBuilder.excludeFromParent(exclusionFilter);
            return this;
        }

        @Override
        public LegacyKernelServicesInitializer excludeResourceFromParent(String exclusionFilter) {
            classLoaderBuilder.excludeResourceFromParent(exclusionFilter);
            return this;
        }

        @Override
        public LegacyKernelServicesInitializer addSingleChildFirstClass(Class<?>...classes) {
            classLoaderBuilder.addSingleChildFirstClass(classes);
            return this;
        }

        private LegacyControllerKernelServicesProxy install(KernelServices mainServices, List<ModelNode> bootOperations) throws Exception {
            if (!skipReverseCheck) {
                bootCurrentVersionWithLegacyBootOperations(bootOperations, mainServices);
            }

            classLoaderBuilder.addParentFirstClassPattern("org.jboss.as.subsystem.bridge.shared.*");

            classLoaderBuilder.addMavenResourceURL("org.wildfly.core:wildfly-subsystem-test-framework:" + ModelTestControllerVersion.CurrentVersion.VERSION);
            classLoaderBuilder.addMavenResourceURL("org.wildfly.core:wildfly-model-test:" + ModelTestControllerVersion.CurrentVersion.VERSION);
            classLoaderBuilder.addMavenResourceURL("org.wildfly.legacy.test:wildfly-legacy-spi:" + Version.LEGACY_TEST_CONTROLLER_VERSION);

            if (testControllerVersion != ModelTestControllerVersion.MASTER && testControllerVersion.getTestControllerVersion() != null) {
                String groupId = testControllerVersion.getCoreMavenGroupId();
                String serverArtifactId = testControllerVersion.getServerMavenArtifactId();
                classLoaderBuilder.addRecursiveMavenResourceURL(groupId + ":" + serverArtifactId + ":" + testControllerVersion.getCoreVersion());

                //TODO Even with this there are some workarounds needed in JGroupsSubsystemTransformerTestCase, InfinispanSubsystemTransformersTestCase and LoggingSubsystemTestCase
                //Don't load modules from the scoped classloader to avoid some funky stuff going on when initializing the JAXP redirect
                //The mentioned funky stuff works fine when running in Eclipse but fails when running the tests on the command-line
                classLoaderBuilder.addParentFirstClassPattern("__redirected.*");
                classLoaderBuilder.addParentFirstClassPattern("org.jboss.modules.*");

                classLoaderBuilder.addMavenResourceURL("org.wildfly.legacy.test:wildfly-legacy-subsystem-" + testControllerVersion.getTestControllerVersion() + ":" + Version.LEGACY_TEST_CONTROLLER_VERSION);
            }
            URLClassLoader legacyCl = classLoaderBuilder.build();

            ScopedKernelServicesBootstrap scopedBootstrap = new ScopedKernelServicesBootstrap(legacyCl);
            return scopedBootstrap.createKernelServices(mainSubsystemName, extensionClassName != null ? extensionClassName : mainExtension.getClass().getName(), additionalInit,
                    getOperationValidationFilter(), bootOperations, modelVersion, persistXml);
        }

        @Override
        public LegacyKernelServicesInitializer dontPersistXml() {
            persistXml = false;
            return this;
        }

        @Override
        public LegacyKernelServicesInitializer skipReverseControllerCheck() {
            skipReverseCheck = true;
            return this;
        }

        @Override
        public LegacyKernelServicesInitializer configureReverseControllerCheck(AdditionalInitialization additionalInit,
                ModelFixer modelFixer) {
            this.reverseCheckConfig = additionalInit;
            this.reverseCheckModelFixer = modelFixer;
            return this;
        }

        @Override
        public LegacyKernelServicesInitializer configureReverseControllerCheck(AdditionalInitialization additionalInit,
                ModelFixer modelFixer, OperationFixer fixer) {
            this.reverseCheckConfig = additionalInit;
            this.reverseCheckModelFixer = modelFixer;
            this.reverseCheckOperationFixer = fixer;
            return this;
        }

        private KernelServices bootCurrentVersionWithLegacyBootOperations(List<ModelNode> bootOperations, KernelServices mainServices) throws Exception {
            //Clone the boot operations to avoid any pollution installing them in the main controller
            List<ModelNode> clonedBootOperations = new ArrayList<ModelNode>();
            for (ModelNode op : bootOperations) {
                ModelNode cloned = reverseCheckOperationFixer.fixOperation(op.clone());
                if (cloned!=null){
                    clonedBootOperations.add(cloned);
                }
            }

            KernelServices reverseServices = createKernelServicesBuilder(reverseCheckConfig)
                .setBootOperations(clonedBootOperations)
                .build();
            final Throwable bootError = reverseServices.getBootError();
            if (bootError != null) {
                if (bootError instanceof Exception) {
                    throw (Exception) bootError;
                }
                throw new Exception(bootError);
            }
            Assert.assertTrue(reverseServices.isSuccessfulBoot());

            ModelNode reverseSubsystem = reverseServices.readWholeModel().get(SUBSYSTEM, getMainSubsystemName());
            if (reverseCheckModelFixer != null) {
                reverseSubsystem = reverseCheckModelFixer.fixModel(reverseSubsystem);
            }
            ModelTestUtils.compare(mainServices.readWholeModel().get(SUBSYSTEM, getMainSubsystemName()), reverseSubsystem);
            return reverseServices;
        }

        private ModelTestOperationValidatorFilter getOperationValidationFilter() {
            if (operationValidationExcludeBuilder != null) {
                return operationValidationExcludeBuilder.build();
            }
            if (additionalInit.isValidateOperations()) {
                return ModelTestOperationValidatorFilter.createValidateAll();
            } else {
                return ModelTestOperationValidatorFilter.createValidateNone();
            }
        }


    }

    @SuppressWarnings("deprecation")
    private final ManagementResourceRegistration MOCK_RESOURCE_REG = new ManagementResourceRegistration() {

        @Override
        public PathAddress getPathAddress() {
            return PathAddress.EMPTY_ADDRESS;
        }

        @Override
        public ProcessType getProcessType() {
            return ProcessType.STANDALONE_SERVER;
        }

        @Override
        public ImmutableManagementResourceRegistration getParent() {
            return null;
        }

        @Override
        public boolean isRuntimeOnly() {
            return false;
        }

        @Override
        public boolean isRemote() {
            return false;
        }

        @Override
        public OperationEntry getOperationEntry(PathAddress address, String operationName) {
            return null;
        }

        @Override
        public OperationStepHandler getOperationHandler(PathAddress address, String operationName) {
            return null;
        }

        @Override
        public DescriptionProvider getOperationDescription(PathAddress address, String operationName) {
            return null;
        }

        @Override
        public Set<Flag> getOperationFlags(PathAddress address, String operationName) {
            return null;
        }

        @Override
        public Set<String> getAttributeNames(PathAddress address) {
            return null;
        }

        @Override
        public AttributeAccess getAttributeAccess(PathAddress address, String attributeName) {
            return null;
        }

        @Override
        public Map<String, AttributeAccess> getAttributes(PathAddress address) {
            return Collections.emptyMap();
        }

        @Override
        public Set<String> getChildNames(PathAddress address) {
            return null;
        }

        @Override
        public Set<PathElement> getChildAddresses(PathAddress address) {
            return null;
        }

        @Override
        public DescriptionProvider getModelDescription(PathAddress address) {
            return null;
        }

        @Override
        public Map<String, OperationEntry> getOperationDescriptions(PathAddress address, boolean inherited) {
            return null;
        }

        @Override
        public Map<String, NotificationEntry> getNotificationDescriptions(PathAddress address, boolean inherited) {
            return null;
        }

        @Override
        public ProxyController getProxyController(PathAddress address) {
            return null;
        }

        @Override
        public Set<ProxyController> getProxyControllers(PathAddress address) {
            return null;
        }

        @Override
        public ManagementResourceRegistration getOverrideModel(String name) {
            return null;
        }

        @Override
        public ManagementResourceRegistration getSubModel(PathAddress address) {
            if (address.size() == 0) {
                return MOCK_RESOURCE_REG;
            } else if (address.size() == 1) {
                PathElement pe = address.getElement(0);
                String key = pe.getKey();
                if (pe.isWildcard()
                        && (ModelDescriptionConstants.PROFILE.equals(key) || ModelDescriptionConstants.DEPLOYMENT.equals(key))) {
                    return MOCK_RESOURCE_REG;
                }
            }
            return null;
        }

        @Override
        public List<AccessConstraintDefinition> getAccessConstraints() {
            return Collections.emptyList();
        }

        @Override
        public ManagementResourceRegistration registerSubModel(ResourceDefinition resourceDefinition) {
            return MOCK_RESOURCE_REG;
        }

        @Override
        public void unregisterSubModel(PathElement address) {
        }

        @Override
        public boolean isAllowsOverride() {
            return true;
        }

        @Override
        public void setRuntimeOnly(boolean runtimeOnly) {
        }

        @Override
        public ManagementResourceRegistration registerOverrideModel(String name, OverrideDescriptionProvider descriptionProvider) {
            return MOCK_RESOURCE_REG;
        }

        @Override
        public void unregisterOverrideModel(String name) {
        }

        @Override
        public void registerOperationHandler(OperationDefinition definition, OperationStepHandler handler) {

        }

        @Override
        public void registerOperationHandler(OperationDefinition definition, OperationStepHandler handler, boolean inherited) {

        }

        @Override
        public void registerCapability(RuntimeCapability capability) {

        }

        @Override
        public void registerIncorporatingCapabilities(Set<RuntimeCapability> capabilities) {

        }

        @Override
        public void registerRequirements(Set<CapabilityReferenceRecorder> requirements) {

        }

        @Override
        public Set<RuntimeCapability> getCapabilities() {
            return Collections.emptySet();
        }

        @Override
        public Set<RuntimeCapability> getIncorporatingCapabilities() {
            return null;
        }

        @Override
        public Set<CapabilityReferenceRecorder> getRequirements() {
            return Collections.emptySet();
        }

        @Override
        public void unregisterOperationHandler(String operationName) {

        }

        @Override
        public void registerReadWriteAttribute(AttributeDefinition definition, OperationStepHandler readHandler, OperationStepHandler writeHandler) {
        }

        @Override
        public void registerReadOnlyAttribute(AttributeDefinition definition, OperationStepHandler readHandler) {
        }

        @Override
        public void registerMetric(AttributeDefinition definition, OperationStepHandler metricHandler) {
        }

        @Override
        public void unregisterAttribute(String attributeName) {
        }

        @Override
        public void registerNotification(NotificationDefinition notification, boolean inherited) {
            // no-op
        }

        @Override
        public void registerNotification(NotificationDefinition notification) {
            // no-op
        }

        @Override
        public void unregisterNotification(String notificationType) {
            // no-op
        }

        @Override
        public boolean isOrderedChildResource() {
            return false;
        }

        @Override
        public Set<String> getOrderedChildTypes() {
            return Collections.emptySet();
        }

        @Override
        public void registerProxyController(PathElement address, ProxyController proxyController) {
        }

        @Override
        public void unregisterProxyController(PathElement address) {
        }

        @Override
        public void registerAlias(PathElement address, AliasEntry alias) {
        }

        @Override
        public void unregisterAlias(PathElement address) {
        }

        @Override
        public AliasEntry getAliasEntry() {
            return null;
        }

        @Override
        public boolean isAlias() {
            return false;
        }

        @Override
        public void registerAdditionalRuntimePackages(RuntimePackageDependency... pkgs) {
            // no-op
        }

        @Override
        public Set<RuntimePackageDependency> getAdditionalRuntimePackages() {
            return Collections.emptySet();
        }
    };


}
