/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2022, Red Hat, Inc., 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.wildfly.test.integration.legacy.microprofile;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.as.arquillian.api.ServerSetup;
import org.jboss.as.arquillian.container.ManagementClient;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.test.integration.management.base.AbstractCliTestBase;
import org.jboss.as.test.shared.ServerReload;
import org.jboss.as.test.shared.SnapshotRestoreSetupTask;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ValueExpression;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.EXTENSION;
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.OUTCOME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.READ_RESOURCE_OPERATION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUCCESS;

@RunWith(Arquillian.class)
@RunAsClient
@ServerSetup({SnapshotRestoreSetupTask.class})
public class LegacyMicroProfileSubsystemMigrationTestCase extends AbstractCliTestBase {
    // Migrating config just results in removal of the subsystem and extension
    private static final LegacyInfo CONFIG =
            new LegacyInfo(
                    "org.wildfly.extension.microprofile.config-smallrye",
                    "microprofile-config-smallrye");
    // Migrating health results in replacement of the subsystem and extension
    private static final LegacyInfo HEALTH =
            new LegacyInfo(
                    "org.wildfly.extension.microprofile.health-smallrye",
                    "microprofile-health-smallrye",
                    new SubsystemInfo("org.wildfly.extension.health", "health"));
    // Migrating metrics results in replacement of the subsystem and extension
    private static final LegacyInfo METRICS =
            new LegacyInfo(
                    "org.wildfly.extension.microprofile.metrics-smallrye",
                    "microprofile-metrics-smallrye",
                    new SubsystemInfo("org.wildfly.extension.metrics", "metrics"));
    // Migrating OpenTracing just results in removal of the subsystem and extension
    private static final LegacyInfo OPEN_TRACING =
            new LegacyInfo(
                    "org.wildfly.extension.microprofile.opentracing-smallrye",
                    "microprofile-opentracing-smallrye",
                    null);

    private static final LegacyInfo[] LEGACY_INFOS = new LegacyInfo[]{CONFIG, HEALTH, METRICS, OPEN_TRACING};


    @ArquillianResource
    protected ManagementClient managementClient;

    @Deployment
    public static WebArchive createEmptyDeployment() {
        // We don't actually want to deploy anything, so just create an empty deployment
        return ShrinkWrap.create(WebArchive.class, "legacy-mp-subsystem-migration.war");
    }

    @Before
    public void removeExtensionsAndSubsystems() throws Exception {
        List<PathAddress> extensions = new ArrayList<>();
        List<PathAddress> subsystems = new ArrayList<>();
        for (LegacyInfo legacyInfo : LEGACY_INFOS) {
            if (legacyInfo.migration != null) {
                extensions.add(legacyInfo.migration.getExtensionAddress());
                subsystems.add(legacyInfo.migration.getSubsystemAddress());
            }
            extensions.add(legacyInfo.getExtensionAddress());
            subsystems.add(legacyInfo.getSubsystemAddress());
        }
        ModelControllerClient client = managementClient.getControllerClient();
        for (PathAddress subsystem : subsystems) {
            if (resourceExists(subsystem)) {
                ModelNode remove = Util.createRemoveOperation(subsystem);
                assertOutcome(client.execute(remove));
            }
        }
        for (PathAddress extension : extensions) {
            if (resourceExists(extension)) {
                ModelNode remove = Util.createRemoveOperation(extension);
                assertOutcome(client.execute(remove));
            }
        }

        // Reload to normal mode since that is what the tests expect
        ServerReload.executeReloadAndWaitForCompletion(managementClient, false);
    }

    @Test
    public void testMigrateConfig() throws Exception {
        // Empty subsystem - we will be removing it
        testMigrateEmptyLegacySubsystem(CONFIG);
    }

    @Test
    public void testMigrateHealthEmpty() throws Exception {
        // Empty subsystem - we will do another test to check migration
        // when populated with attributes
        testMigrateEmptyLegacySubsystem(HEALTH);
    }

    @Test
    public void testMigrateHealthPopulated() throws Exception {
        /**
         *            security-enabled="${security-enabled:true}"
         *            empty-liveness-checks-status="${empty-liveness-checks-status:UP}"
         *            empty-readiness-checks-status="${empty-readiness-checks-status:UP}" />
         */
        ModelNode addOp = Util.createAddOperation(HEALTH.getSubsystemAddress());
        addOp.get("security-enabled").set(new ValueExpression("${security-enabled:true}"));
        addOp.get("empty-liveness-checks-status").set(new ValueExpression("${empty-liveness-checks-status:UP}"));
        addOp.get("empty-readiness-checks-status").set(new ValueExpression("${empty-readiness-checks-status:UP}"));

        testMigrateLegacySubsystem(HEALTH, addOp, (ModelNode migratedResource) -> {
            Set<String> keys = migratedResource.keys();
            Assert.assertEquals(1, keys.size());
            Assert.assertEquals(addOp.get("security-enabled"), migratedResource.get("security-enabled"));
        });


    }

    @Test
    public void testMigrateMetricsEmpty() throws Exception {
        // Empty subsystem - we will do another test to check migration
        // when populated with attributes
        testMigrateEmptyLegacySubsystem(METRICS);
    }

    @Test
    public void testMigrateMetricsPopulated() throws Exception {
        // Populated subsystem. All attributes should be populated
        ModelNode addOp = Util.createAddOperation(METRICS.getSubsystemAddress());
        addOp.get("security-enabled").set(new ValueExpression("${security-enabled:true}"));
        addOp.get("exposed-subsystems").add("undertow");
        addOp.get("prefix").set(new ValueExpression("${wildfly.metrics.prefix:wildfly}"));

        testMigrateLegacySubsystem(METRICS, addOp, (ModelNode migratedResource) -> {
            ModelNode check = addOp.clone();
            check.remove(OP);
            check.remove(OP_ADDR);
            Assert.assertEquals(check, migratedResource);
        });
    }

    @Test
    public void testMigrateOpenTracing() throws Exception {
        // Empty subsystem - we will be removing it
        testMigrateEmptyLegacySubsystem(OPEN_TRACING);
    }


    private void testMigrateEmptyLegacySubsystem(LegacyInfo info) throws Exception {
        testMigrateLegacySubsystem(info, null, null);
    }

    private void testMigrateLegacySubsystem(LegacyInfo info, ModelNode legacySubsystemAdd, Consumer<ModelNode> migratedResourceChecker) throws Exception {
        // Check none of the subsystems are there
        assertResourceDoesNotExist(info.getExtensionAddress());
        assertResourceDoesNotExist(info.getSubsystemAddress());
        if (info.migration != null) {
            assertResourceDoesNotExist(info.migration.getExtensionAddress());
            assertResourceDoesNotExist(info.migration.getSubsystemAddress());
        }

        // Server is in normal mode - adding extension should break
        ModelControllerClient client = managementClient.getControllerClient();
        ModelNode addLegacyExtension = Util.createAddOperation(info.getExtensionAddress());
        assertOutcomeFailed(client.execute(addLegacyExtension));

        //Reboot to admin-only. Now we should be able to add both the legacy extension and subsystem
        ServerReload.executeReloadAndWaitForCompletion(managementClient, true);
        assertOutcome(client.execute(addLegacyExtension));
        ModelNode addLegacySubsystem = legacySubsystemAdd == null ?
                Util.createAddOperation(info.getSubsystemAddress()) : legacySubsystemAdd;
        assertOutcome(client.execute(addLegacySubsystem));

        // Call the migrate operation
        ModelNode migrate = Util.createEmptyOperation("migrate", info.getSubsystemAddress());
        assertOutcome(client.execute(migrate));

        // Check legacy subsystem was removed
        assertResourceDoesNotExist(info.getExtensionAddress());
        assertResourceDoesNotExist(info.getSubsystemAddress());
        if (info.migration != null) {
            // Health and metrics get the new subsystem added
            assertResourceExists(info.migration.getExtensionAddress());
            assertResourceExists(info.migration.getSubsystemAddress());

            if (migratedResourceChecker != null) {
                ModelNode op = Util.getEmptyOperation(READ_RESOURCE_OPERATION, info.migration.getSubsystemAddress().toModelNode());
                ModelNode response = managementClient.getControllerClient().execute(op);
                migratedResourceChecker.accept(getResult(response));
            }
        }
    }

    private void assertResourceExists(PathAddress pathAddress) throws Exception {
        Assert.assertTrue(resourceExists(pathAddress));
    }

    private void assertResourceDoesNotExist(PathAddress pathAddress) throws Exception {
        Assert.assertFalse(resourceExists(pathAddress));
    }

    private boolean resourceExists(PathAddress pathAddress) throws Exception {
        ModelNode op = Util.getEmptyOperation(READ_RESOURCE_OPERATION, pathAddress.toModelNode());
        ModelNode response = managementClient.getControllerClient().execute(op);
        return checkOutcome(response);
    }

    private void assertOutcome(ModelNode response) {
        Assert.assertTrue(response.asString(), checkOutcome(response));
    }

    private void assertOutcomeFailed(ModelNode response) {
        Assert.assertFalse(response.asString(), checkOutcome(response));
    }

    private boolean checkOutcome(ModelNode response) {
        return response.get(OUTCOME).asString().equals(SUCCESS);
    }

    private ModelNode getResult(ModelNode response) {
        assertOutcome(response);
        return response.get(RESULT);
    }

    private static class SubsystemInfo {
        private final String extensionName;
        private final String subsystemName;

        SubsystemInfo(String extensionName, String subsystemName) {
            this.extensionName = extensionName;
            this.subsystemName = subsystemName;
        }

        PathAddress getExtensionAddress() {
            return PathAddress.pathAddress(EXTENSION, extensionName);
        }

        PathAddress getSubsystemAddress() {
            return PathAddress.pathAddress(SUBSYSTEM, subsystemName);
        }
    }

    private static class LegacyInfo extends SubsystemInfo {
        private final SubsystemInfo migration;

        LegacyInfo(String extensionName, String subsystemName) {
            this(extensionName, subsystemName, null);
        }

        LegacyInfo(String extensionName, String subsystemName, SubsystemInfo migration) {
            super(extensionName, subsystemName);
            this.migration = migration;
        }
    }
}
