/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.as.test.manualmode.deployment;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD;
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.REMOVE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import jakarta.inject.Inject;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;

import org.jboss.as.controller.client.helpers.Operations;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.protocol.StreamUtils;
import org.jboss.as.test.deployment.trivial.ServiceActivatorDeploymentUtil;
import org.jboss.as.test.shared.TimeoutUtil;
import org.jboss.byteman.agent.submit.Submit;
import org.jboss.dmr.ModelNode;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.wildfly.core.testrunner.ServerControl;
import org.wildfly.core.testrunner.ServerController;
import org.wildfly.core.testrunner.UnsuccessfulOperationException;
import org.wildfly.core.testrunner.WildFlyRunner;

/**
 * Tests server reload during a deployment or undeployment to a wildfly core server by the filesystem scanner.
 *
 * @author <a href="mailto:ehugonne@redhat.com">Emmanuel Hugonnet</a> (c) 2015 Red Hat, inc.
 */
@RunWith(WildFlyRunner.class)
@ServerControl(manual = true)
@Ignore("this test relies too much on hacking internals (see the byteman script) so it's only here to allow " +
        "custom use if there is a need to diagnose a problem in this area")
public class DeploymentScannerShutdownTestCase {

    // Max time to wait for some action to complete, in ms
    private static final int TIMEOUT = TimeoutUtil.adjust(20000);
    // Pause time between checks whether some action has completed, in ms
    private static final int BACKOFF = 10;
    private static final String DEPLOYMENT_SCANNER_EXTENSION = "org.jboss.as.deployment-scanner";
    private static final String DEPLOYMENT_SCANNER_SUBSYSTEM = "deployment-scanner";
    private static final String DEPLOYMENT_NAME = "test-deployment.jar";

    @Inject
    private ServerController container;

    private File deployDir;
    private final String scannerName = "autoZips";
    private static final Map<String, String> properties = new HashMap<>();

    private final Submit bytemanSubmit = new Submit(
            System.getProperty("byteman.server.ipaddress", Submit.DEFAULT_ADDRESS),
            Integer.getInteger("byteman.server.port", Submit.DEFAULT_PORT));

    @BeforeClass
    public static void addDeploymentScanner() throws Exception {
        properties.clear();
        properties.put("service", "is new");
        properties.clear();
        properties.put("service", "is replaced");
    }

    @After
    public void cleanAll() throws Exception {
        try {
            removeRules();
        } catch (Exception e) {

        }
        try {
            removeDeploymentScanner(scannerName);
        } catch (Exception e) {

        }
        try {
            removeDeploymentScannerExtension();
        } catch (Exception e) {

        }
        cleanFile(deployDir);
        deployDir.delete();
        container.stop();
    }

    @Before
    public void prepareServer() throws Exception {
        deployDir = createDeploymentDir("auto-deployments");
        container.start();
        addDeploymentScannerExtension();
        addDeploymentScanner(deployDir, scannerName, true);
    }

    private void deployRules() throws Exception {
        bytemanSubmit.addRulesFromResources(Collections.singletonList(
                DeploymentScannerShutdownTestCase.class.getClassLoader().getResourceAsStream("byteman/DeploymentScannerShutdownTestCase.btm")));
    }

    private void removeRules() {
        try {
            bytemanSubmit.deleteAllRules();
        } catch (Exception ex) {

        }
    }

    @Test
    public void testFilesystemDeployment() throws Exception {
        deployRules();
        final JavaArchive archive = ServiceActivatorDeploymentUtil.createServiceActivatorDeploymentArchive(DEPLOYMENT_NAME, properties);
        final File dir = new File("target/archives");
        dir.mkdirs();
        final File deployed = new File(deployDir, DEPLOYMENT_NAME + ".deployed");
        final File target = new File(deployDir, DEPLOYMENT_NAME);
        final File file = new File(dir, DEPLOYMENT_NAME);
        archive.as(ZipExporter.class).exportTo(file, true);
        deploy(file, target, deployed);
        container.reload(TIMEOUT);
        Assert.assertTrue("We should have the deployed marker", deployed.exists());
        Assert.assertTrue(container.isStarted());
        Path serverLog = getAbsoluteLogFilePath("jboss.server.log.dir", "server.log");
        assertLogContains(serverLog, "rejected from java.util.concurrent.ScheduledThreadPoolExecutor", false);
        container.stop();
        deployed.delete();
        target.delete();
        container.start();
        removeRules();
    }

    @Test
    public void testFileSystemUndeployment() throws Exception {
        final JavaArchive archive = ServiceActivatorDeploymentUtil.createServiceActivatorDeploymentArchive("test-deployment.jar", properties);
        final File dir = new File("target/archives");
        dir.mkdirs();
        final File deployed = new File(deployDir, DEPLOYMENT_NAME + ".deployed");
        final File undeployed = new File(deployDir, DEPLOYMENT_NAME + ".undeployed");
        if (undeployed.exists()) {
            undeployed.delete();
        }
        final File target = new File(deployDir, DEPLOYMENT_NAME);
        final File file = new File(dir,DEPLOYMENT_NAME);
        archive.as(ZipExporter.class).exportTo(file, true);
        removeRules();
        deploy(file, target, deployed);
        deployRules();
        undeploy(target, undeployed);
        container.reload(TIMEOUT);
        Assert.assertTrue("We should have the undeployed marker", undeployed.exists());
        Assert.assertTrue(container.isStarted());
        Path serverLog = getAbsoluteLogFilePath("jboss.server.log.dir", "server.log");
        assertLogContains(serverLog, "rejected from java.util.concurrent.ScheduledThreadPoolExecutor", false);
        //container.start();
    }

    private void assertLogContains(final Path logFile, final String msg, final boolean expected) throws Exception {
        try (final BufferedReader reader = Files.newBufferedReader(logFile, StandardCharsets.UTF_8)) {
            String line;
            boolean logFound = false;

            while ((line = reader.readLine()) != null) {
                if (line.contains(msg)) {
                    logFound = true;
                    break;
                }
            }
            Assert.assertTrue(logFound == expected);
        }
    }

    private void deploy(final File file, final File target, final File deployed) throws IOException {
        Assert.assertFalse(target.exists());
        // Copy file to deploy directory
        final InputStream in = new BufferedInputStream(new FileInputStream(file));
        try {
            final OutputStream out = new BufferedOutputStream(new FileOutputStream(target));
            try {
                int i = in.read();
                while (i != -1) {
                    out.write(i);
                    i = in.read();
                }
            } finally {
                StreamUtils.safeClose(out);
            }
        } finally {
            StreamUtils.safeClose(in);
        }
        Assert.assertTrue(file.exists());
        waitForMarkerFile(deployed);
    }

    private void undeploy(final File target, final File undeployed) throws IOException {
        // Delete file from deploy directory
        target.delete();
        waitForMarkerFile(undeployed);
    }

    private void waitForMarkerFile(File marker) {
        for (int i = 0; i < TIMEOUT / BACKOFF; i++) {
            if (marker.exists()) {
                break;
            }
            try {
                Thread.sleep(BACKOFF);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            }
        }
    }

    private ModelNode addDeploymentScanner(final File deployDir, final String scannerName, final boolean autoDeployZipped)
            throws Exception {
        ModelNode add = new ModelNode();
        add.get(OP).set(ADD);
        ModelNode addr = new ModelNode();
        addr.add("subsystem", "deployment-scanner");
        addr.add("scanner", scannerName);
        add.get(OP_ADDR).set(addr);
        add.get("path").set(deployDir.getAbsolutePath());
        add.get("scan-enabled").set(true);
        add.get("scan-interval").set(1000);
        if (autoDeployZipped == false) {
            add.get("auto-deploy-zipped").set(false);
        }
        return container.getClient().executeForResult(add);
    }

    private void removeDeploymentScanner(final String scannerName) throws Exception {
        ModelNode addr = new ModelNode();
        addr.add("subsystem", "deployment-scanner");
        addr.add("scanner", scannerName);
        ModelNode remove = new ModelNode();
        remove.get(OP).set(REMOVE);
        remove.get(OP_ADDR).set(addr);
        container.getClient().executeForResult(remove);
    }

    private File createDeploymentDir(String dir) {
        deployDir = new File("target", dir);
        cleanFile(deployDir);
        deployDir.mkdirs();
        Assert.assertTrue(deployDir.exists());
        return deployDir;
    }

    private static void cleanFile(File toClean) {
        if (toClean.isDirectory()) {
            for (File child : toClean.listFiles()) {
                cleanFile(child);
            }
        }
        toClean.delete();
    }

    private Path getAbsoluteLogFilePath(final String relativePath, final String fileName) throws UnsuccessfulOperationException {
        final ModelNode address = PathAddress.pathAddress(
                PathElement.pathElement(ModelDescriptionConstants.PATH, relativePath)).toModelNode();
        final ModelNode result;
        try {
            final ModelNode op = Operations.createReadAttributeOperation(address, ModelDescriptionConstants.PATH);
            result = container.getClient().executeForResult(op);
            return Paths.get(result.asString(), fileName);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    private void addDeploymentScannerExtension() throws Exception {
        ModelNode addOp = Util.createAddOperation(PathAddress.pathAddress(PathElement.pathElement(EXTENSION, DEPLOYMENT_SCANNER_EXTENSION)));
        container.getClient().executeForResult(addOp);
        addOp = Util.createAddOperation(PathAddress.pathAddress(PathElement.pathElement(SUBSYSTEM, DEPLOYMENT_SCANNER_SUBSYSTEM)));
        container.getClient().executeForResult(addOp);
    }

    private void removeDeploymentScannerExtension() throws Exception {
        try {
            ModelNode removeOp = Util.createRemoveOperation(PathAddress.pathAddress(PathElement.pathElement(SUBSYSTEM, DEPLOYMENT_SCANNER_SUBSYSTEM)));
            container.getClient().executeForResult(removeOp);
        } finally {
            ModelNode removeOp = Util.createRemoveOperation(PathAddress.pathAddress(PathElement.pathElement(EXTENSION, DEPLOYMENT_SCANNER_EXTENSION)));
            container.getClient().executeForResult(removeOp);
        }
    }
}
