/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.as.clustering.infinispan.subsystem;

import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.jboss.as.clustering.controller.CommonServiceDescriptor;
import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteCacheContainerResourceDefinition;
import org.jboss.as.clustering.jgroups.subsystem.JGroupsSubsystemInitialization;
import org.jboss.as.controller.ModelVersion;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.services.path.PathManager;
import org.jboss.as.model.test.FailedOperationTransformationConfig;
import org.jboss.as.model.test.ModelFixer;
import org.jboss.as.model.test.ModelTestControllerVersion;
import org.jboss.as.model.test.ModelTestUtils;
import org.jboss.as.network.OutboundSocketBinding;
import org.jboss.as.server.ServerEnvironment;
import org.jboss.as.subsystem.test.AdditionalInitialization;
import org.jboss.as.subsystem.test.KernelServices;
import org.jboss.as.subsystem.test.KernelServicesBuilder;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.Property;
import org.jgroups.conf.ClassConfigurator;
import org.junit.Assert;
import org.junit.Test;
import org.wildfly.clustering.jgroups.spi.ChannelFactory;

/**
 * Test cases for transformers used in the Infinispan subsystem
 *
 * Here we perform two types of tests:
 * - testing transformation between the current model and legacy models, in the case
 * where no rejections are expected, but certain discards/conversions/renames are expected
 * - testing transformation between the current model and legacy models, in the case
 * where specific rejections are expected
 *
 * @author <a href="tomaz.cerar@redhat.com">Tomaz Cerar</a>
 * @author Richard Achmatowicz (c) 2011 Red Hat Inc.
 * @author Radoslav Husar
 */
public class InfinispanTransformersTestCase extends OperationTestCaseBase {

    private static String formatArtifact(String pattern, ModelTestControllerVersion version) {
        return String.format(pattern, version.getMavenGavVersion());
    }

    private static InfinispanSubsystemModel getModelVersion(ModelTestControllerVersion controllerVersion) {
        switch (controllerVersion) {
            case EAP_7_4_0:
                return InfinispanSubsystemModel.VERSION_14_0_0;
            default:
                throw new IllegalArgumentException();
        }
    }

    private static String[] getDependencies(ModelTestControllerVersion version) {
        switch (version) {
            case EAP_7_4_0:
                return new String[] {
                        "org.jboss.spec.javax.transaction:jboss-transaction-api_1.3_spec:2.0.0.Final",
                        "org.jboss.spec.javax.resource:jboss-connector-api_1.7_spec:2.0.0.Final",
                        formatArtifact("org.jboss.eap:wildfly-clustering-infinispan-extension:%s", version),
                        "org.infinispan:infinispan-commons:11.0.9.Final-redhat-00001",
                        "org.infinispan:infinispan-core:11.0.9.Final-redhat-00001",
                        "org.infinispan:infinispan-cachestore-jdbc:11.0.9.Final-redhat-00001",
                        "org.infinispan:infinispan-client-hotrod:11.0.9.Final-redhat-00001",
                        "org.jboss.spec.javax.resource:jboss-connector-api_1.7_spec:2.0.0.Final-redhat-00001",
                        // Following are needed for InfinispanSubsystemInitialization
                        formatArtifact("org.jboss.eap:wildfly-clustering-api:%s", version),
                        formatArtifact("org.jboss.eap:wildfly-clustering-common:%s", version),
                        formatArtifact("org.jboss.eap:wildfly-clustering-infinispan-client:%s", version),
                        formatArtifact("org.jboss.eap:wildfly-clustering-infinispan-spi:%s", version),
                        formatArtifact("org.jboss.eap:wildfly-clustering-jgroups-extension:%s", version),
                        formatArtifact("org.jboss.eap:wildfly-clustering-jgroups-spi:%s", version),
                        formatArtifact("org.jboss.eap:wildfly-clustering-server:%s", version),
                        formatArtifact("org.jboss.eap:wildfly-clustering-service:%s", version),
                        formatArtifact("org.jboss.eap:wildfly-clustering-singleton-api:%s", version),
                        formatArtifact("org.jboss.eap:wildfly-clustering-spi:%s", version),
                        formatArtifact("org.jboss.eap:wildfly-connector:%s", version),
                };
            default: {
                throw new IllegalArgumentException();
            }
        }
    }

    @Override
    AdditionalInitialization createAdditionalInitialization() {
        return new InfinispanSubsystemInitialization()
                .require(PathManager.SERVICE_DESCRIPTOR)
                .require(PathManager.PATH_SERVICE_DESCRIPTOR, ServerEnvironment.SERVER_TEMP_DIR)
                .require(OutboundSocketBinding.SERVICE_DESCRIPTOR, "hotrod-server-1")
                .require(OutboundSocketBinding.SERVICE_DESCRIPTOR, "hotrod-server-2")
                .require(OutboundSocketBinding.SERVICE_DESCRIPTOR, "jdg1")
                .require(OutboundSocketBinding.SERVICE_DESCRIPTOR, "jdg2")
                .require(OutboundSocketBinding.SERVICE_DESCRIPTOR, "jdg3")
                .require(OutboundSocketBinding.SERVICE_DESCRIPTOR, "jdg4")
                .require(OutboundSocketBinding.SERVICE_DESCRIPTOR, "jdg5")
                .require(OutboundSocketBinding.SERVICE_DESCRIPTOR, "jdg6")
                .require(CommonServiceDescriptor.DATA_SOURCE, "ExampleDS")
                .require(CommonServiceDescriptor.SSL_CONTEXT, "hotrod-elytron")
                .require(ChannelFactory.SERVICE_DESCRIPTOR, "maximal-channel")
                .require(ChannelFactory.DEFAULT_SERVICE_DESCRIPTOR)
                .require(TransactionResourceDefinition.LOCAL_TRANSACTION_PROVIDER)
                .require(TransactionResourceDefinition.XA_RESOURCE_RECOVERY_REGISTRY)
                ;
    }

    @Test
    public void testTransformerEAP740() throws Exception {
        testTransformation(ModelTestControllerVersion.EAP_7_4_0);
    }

    private KernelServices buildKernelServices(String xml, ModelTestControllerVersion controllerVersion, ModelVersion version, String... mavenResourceURLs) throws Exception {
        KernelServicesBuilder builder = this.createKernelServicesBuilder().setSubsystemXml(xml);

        builder.createLegacyKernelServicesBuilder(createAdditionalInitialization(), controllerVersion, version)
                .addMavenResourceURL(mavenResourceURLs)
                .addSingleChildFirstClass(InfinispanSubsystemInitialization.class)
                .addSingleChildFirstClass(JGroupsSubsystemInitialization.class)
                .addSingleChildFirstClass(org.jboss.as.clustering.subsystem.AdditionalInitialization.class)
                .addSingleChildFirstClass(ClassConfigurator.class)
                .skipReverseControllerCheck()
                .dontPersistXml();

        KernelServices services = builder.build();
        Assert.assertTrue(ModelTestControllerVersion.MASTER + " boot failed", services.isSuccessfulBoot());
        Assert.assertTrue(controllerVersion.getMavenGavVersion() + " boot failed", services.getLegacyServices(version).isSuccessfulBoot());
        return services;
    }

    private void testTransformation(final ModelTestControllerVersion controller) throws Exception {
        final ModelVersion version = getModelVersion(controller).getVersion();
        final String[] dependencies = getDependencies(controller);
        final String subsystemXmlResource = String.format("subsystem-infinispan-transform-%d_%d_%d.xml", version.getMajor(), version.getMinor(), version.getMicro());

        KernelServices services = this.buildKernelServices(readResource(subsystemXmlResource), controller, version, dependencies);

        // check that both versions of the legacy model are the same and valid
        checkSubsystemModelTransformation(services, version, createModelFixer(version), false);

        // Validate transformed model
        ModelNode legacyModel = services.readTransformedModel(version);
        if (InfinispanSubsystemModel.VERSION_18_0_0.requiresTransformation(version)) {
            ModelNode subsystemModel = legacyModel.get(InfinispanSubsystemResourceDefinition.PATH.getKeyValuePair());
            // Verify module conversions to legacy module names
            for (Property property : subsystemModel.get(CacheContainerResourceDefinition.WILDCARD_PATH.getKey()).asPropertyListOrEmpty()) {
                List<ModelNode> modules = property.getValue().get(CacheContainerResourceDefinition.ListAttribute.MODULES.getName()).asListOrEmpty();
                Assert.assertTrue(modules.stream().map(ModelNode::asString).noneMatch("org.wildfly.clustering.session.infinispan.embedded"::equals));
            }
            for (Property property : subsystemModel.get(RemoteCacheContainerResourceDefinition.WILDCARD_PATH.getKey()).asPropertyList()) {
                List<ModelNode> modules = property.getValue().get(RemoteCacheContainerResourceDefinition.ListAttribute.MODULES.getName()).asListOrEmpty();
                Assert.assertTrue(modules.stream().map(ModelNode::asString).noneMatch("org.wildfly.clustering.session.infinispan.remote"::equals));
            }
        }
    }

    private static ModelFixer createModelFixer(ModelVersion version) {
        return model -> {
            if (InfinispanSubsystemModel.VERSION_16_0_0.requiresTransformation(version)) {
                @SuppressWarnings("deprecation")
                Map<String, List<PathElement>> containers = Map.ofEntries(Map.entry("minimal", List.of(DistributedCacheResourceDefinition.pathElement("dist"))),
                        Map.entry("maximal", List.of(DistributedCacheResourceDefinition.pathElement("dist"), LocalCacheResourceDefinition.pathElement("local"), ReplicatedCacheResourceDefinition.pathElement("cache-with-jdbc-store"), ScatteredCacheResourceDefinition.pathElement("scattered"))));
                for (Map.Entry<String, List<PathElement>> entry : containers.entrySet()) {
                    PathElement containerPath = CacheContainerResourceDefinition.pathElement(entry.getKey());
                    ModelNode containerModel = model.get(containerPath.getKeyValuePair());
                    containerModel.get("module").set(new ModelNode());
                    for (PathElement cachePath : entry.getValue()) {
                        ModelNode cacheModel = containerModel.get(cachePath.getKey()).get(cachePath.getValue());
                        cacheModel.get("module").set(new ModelNode());
                        if (cacheModel.hasDefined(HeapMemoryResourceDefinition.PATH.getKeyValuePair())) {
                            ModelNode memoryModel = cacheModel.get(HeapMemoryResourceDefinition.PATH.getKeyValuePair());
                            memoryModel.get("max-entries").set(new ModelNode());
                        }
                        if (cacheModel.hasDefined(JDBCStoreResourceDefinition.PATH.getKeyValuePair())) {
                            ModelNode storeModel = cacheModel.get(JDBCStoreResourceDefinition.PATH.getKeyValuePair());
                            storeModel.get("datasource").set(new ModelNode());
                            if (storeModel.hasDefined(StringTableResourceDefinition.PATH.getKeyValuePair())) {
                                ModelNode tableModel = storeModel.get(StringTableResourceDefinition.PATH.getKeyValuePair());
                                tableModel.get("batch-size").set(new ModelNode());
                            }
                        }
                    }
                    for (ScheduledThreadPoolResourceDefinition pool : EnumSet.allOf(ScheduledThreadPoolResourceDefinition.class)) {
                        if (containerModel.hasDefined(pool.getPathElement().getKeyValuePair())) {
                            ModelNode poolModel = containerModel.get(pool.getPathElement().getKeyValuePair());
                            poolModel.get("max-threads").set(new ModelNode());
                        }
                    }
                }
            }
            return model;
        };
    }
    @Test
    public void testRejectionsEAP740() throws Exception {
        testRejections(ModelTestControllerVersion.EAP_7_4_0);
    }

    private void testRejections(final ModelTestControllerVersion controller) throws Exception {
        final ModelVersion version = getModelVersion(controller).getVersion();
        final String[] dependencies = getDependencies(controller);

        // create builder for current subsystem version
        KernelServicesBuilder builder = this.createKernelServicesBuilder();

        // initialize the legacy services
        builder.createLegacyKernelServicesBuilder(createAdditionalInitialization(), controller, version)
                .addSingleChildFirstClass(InfinispanSubsystemInitialization.class)
                .addSingleChildFirstClass(JGroupsSubsystemInitialization.class)
                .addSingleChildFirstClass(org.jboss.as.clustering.subsystem.AdditionalInitialization.class)
                .addSingleChildFirstClass(ClassConfigurator.class)
                .addMavenResourceURL(dependencies)
                //TODO storing the model triggers the weirdness mentioned in SubsystemTestDelegate.LegacyKernelServiceInitializerImpl.install()
                //which is strange since it should be loading it all from the current jboss modules
                //Also this works in several other tests
                .dontPersistXml();

        KernelServices services = builder.build();
        KernelServices legacyServices = services.getLegacyServices(version);
        Assert.assertNotNull(legacyServices);
        Assert.assertTrue("main services did not boot", services.isSuccessfulBoot());
        Assert.assertTrue(legacyServices.isSuccessfulBoot());

        // test failed operations involving backups
        List<ModelNode> operations = builder.parseXmlResource("subsystem-infinispan-transformer-reject.xml");
        ModelTestUtils.checkFailedTransformedBootOperations(services, version, operations, createFailedOperationConfig(version));
    }

    private static FailedOperationTransformationConfig createFailedOperationConfig(ModelVersion version) {

        FailedOperationTransformationConfig config = new FailedOperationTransformationConfig();
        PathAddress subsystemAddress = PathAddress.pathAddress(InfinispanSubsystemResourceDefinition.PATH);
        PathAddress containerAddress = subsystemAddress.append(CacheContainerResourceDefinition.WILDCARD_PATH);
        PathAddress remoteContainerAddress = subsystemAddress.append(RemoteCacheContainerResourceDefinition.WILDCARD_PATH);
        List<String> rejectedRemoteContainerAttributes = new LinkedList<>();

        if (InfinispanSubsystemModel.VERSION_16_0_0.requiresTransformation(version)) {
            config.addFailedAttribute(containerAddress.append(ReplicatedCacheResourceDefinition.pathElement("repl"), PartitionHandlingResourceDefinition.PATH), new FailedOperationTransformationConfig.NewAttributesConfig(PartitionHandlingResourceDefinition.Attribute.MERGE_POLICY.getDefinition()));
            config.addFailedAttribute(containerAddress.append(DistributedCacheResourceDefinition.pathElement("dist"), PartitionHandlingResourceDefinition.PATH), new FailedOperationTransformationConfig.NewAttributesConfig(PartitionHandlingResourceDefinition.Attribute.WHEN_SPLIT.getDefinition()));
        }

        if (InfinispanSubsystemModel.VERSION_15_0_0.requiresTransformation(version)) {
            config.addFailedAttribute(containerAddress, new FailedOperationTransformationConfig.NewAttributesConfig(CacheContainerResourceDefinition.Attribute.MARSHALLER.getDefinition()));
            rejectedRemoteContainerAttributes.add(RemoteCacheContainerResourceDefinition.Attribute.MARSHALLER.getName());
        }

        if (InfinispanSubsystemModel.VERSION_14_0_0.requiresTransformation(version)) {
            rejectedRemoteContainerAttributes.add(RemoteCacheContainerResourceDefinition.ListAttribute.MODULES.getName());
        }

        if (!rejectedRemoteContainerAttributes.isEmpty()) {
            config.addFailedAttribute(remoteContainerAddress, new FailedOperationTransformationConfig.NewAttributesConfig(rejectedRemoteContainerAttributes.toArray(new String[rejectedRemoteContainerAttributes.size()])));
        }

        return config;
    }
}
