/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, 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.test.smoke.jsr88;

import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.as.ee.deployment.spi.DeploymentManagerImpl;
import org.jboss.as.ee.deployment.spi.DeploymentMetaData;
import org.jboss.as.ee.deployment.spi.JarUtils;
import org.jboss.as.ee.deployment.spi.factories.DeploymentFactoryImpl;
import org.jboss.as.test.http.Authentication;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.EnterpriseArchive;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;

import javax.enterprise.deploy.shared.ModuleType;
import javax.enterprise.deploy.shared.StateType;
import javax.enterprise.deploy.shared.factories.DeploymentFactoryManager;
import javax.enterprise.deploy.spi.DeploymentManager;
import javax.enterprise.deploy.spi.Target;
import javax.enterprise.deploy.spi.TargetModuleID;
import javax.enterprise.deploy.spi.exceptions.TargetException;
import javax.enterprise.deploy.spi.factories.DeploymentFactory;
import javax.enterprise.deploy.spi.status.DeploymentStatus;
import javax.enterprise.deploy.spi.status.ProgressEvent;
import javax.enterprise.deploy.spi.status.ProgressListener;
import javax.enterprise.deploy.spi.status.ProgressObject;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarOutputStream;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Deployment API JSR-88 tests
 *
 * @author Thomas.Diesler@jboss.com
 * @since 02-Aug-2011
 */
@RunAsClient
@RunWith(Arquillian.class)
public class EnterpriseDeploymentTestCase {

    private static final long TIMEOUT = 10000;
    private static final String WAR_JBOSS_FILE = "WEB-INF/jboss-web.xml";
    private static final String JAR_JBOSS_FILE = "META-INF/jboss.xml";
    private static final String EAR_JBOSS_FILE = "META-INF/jboss-app.xml";

    @Test
    public void testDeploymentManager() throws Exception {
        DeploymentManager manager = getDeploymentManager();
        assertNotNull("DeploymentManager not null", manager);
        Target target = manager.getTargets()[0];
        assertEquals("ServerDeploymentManager target", target.getDescription());
    }

    @Test
    public void testDistributeWebApp() throws Exception {
        DeploymentManager manager = getDeploymentManager();
        ProgressObject progress = jsr88Deploy(manager, getWebArchive());
        TargetModuleID[] targetModules = progress.getResultTargetModuleIDs();
        try {
            DeploymentStatus state = progress.getDeploymentStatus();
            assertEquals(StateType.COMPLETED, state.getState());
            assertServletAccess("custom-context");
        } finally {
            jsr88Undeploy(manager, targetModules);
        }
        try {
            assertServletAccess("custom-context");
            fail("Test deployment not undeployed");
        } catch (IOException e) {
            // ignore
        }
    }

    @Test
    public void testDistributeBadWar() throws Exception {
        DeploymentManager manager = getDeploymentManager();
        ProgressObject progress = jsr88Deploy(manager, getBadWebArchive());
        TargetModuleID[] targetModules = progress.getResultTargetModuleIDs();
        try {
            DeploymentStatus state = progress.getDeploymentStatus();
            assertEquals(StateType.FAILED, state.getState());
        } finally {
            jsr88Undeploy(manager, targetModules);
        }
    }

    @Test
    public void testDistributeEjbApp() throws Exception {
        DeploymentManager manager = getDeploymentManager();
        ProgressObject progress = jsr88Deploy(manager, getEjbArchive());
        TargetModuleID[] targetModules = progress.getResultTargetModuleIDs();
        try {
            DeploymentStatus state = progress.getDeploymentStatus();
            assertEquals(StateType.COMPLETED, state.getState());
        } finally {
            jsr88Undeploy(manager, targetModules);
        }
    }

    @Test
    public void testDistributeEARApp() throws Exception {
        DeploymentManager manager = getDeploymentManager();
        ProgressObject progress = jsr88Deploy(manager, getEarArchive());
        TargetModuleID[] targetModules = progress.getResultTargetModuleIDs();
        try {
            DeploymentStatus state = progress.getDeploymentStatus();
            assertEquals(StateType.COMPLETED, state.getState());
            assertServletAccess("custom-context");
        } finally {
            jsr88Undeploy(manager, targetModules);
        }
        try {
            assertServletAccess("custom-context");
            fail("Test deployment not undeployed");
        } catch (Exception e) {
            // ignore
        }
    }

    @Test
    public void testListAvailableModules() throws Exception {
        String uri = DeploymentManagerImpl.DEPLOYER_URI + "?targetType=as7&serverHost=127.0.0.1&serverPort=9999";
        DeploymentManager manager = getDeploymentManager(uri, Authentication.USERNAME, Authentication.PASSWORD);
        Target[] targets = manager.getTargets();
        TargetModuleID[] modules = manager.getAvailableModules(ModuleType.EAR, targets);
        assertNull(modules);

        ProgressObject progress = jsr88Deploy(manager, getEarArchive());
        TargetModuleID[] targetModules = progress.getResultTargetModuleIDs();
        try {
            // Test getAvailableModules
            modules = manager.getAvailableModules(ModuleType.EAR, targets);
            assertNotNull(modules);
            assertEquals(1, modules.length);

            TargetModuleID targetModuleID = modules[0];
            String moduleID = targetModuleID.getModuleID();
            assertTrue("Ends with deployment-app.ear", moduleID.endsWith("deployment-app.ear"));

            // Test getNonRunningModules
            modules = manager.getNonRunningModules(ModuleType.EAR, targets);
            assertEquals("non-running EAR modules count expected to be zero " + modules, 0, modules.length);

            // Test getRunningModules
            modules = manager.getRunningModules(ModuleType.EAR, targets);
            assertEquals("running EAR modules count expected to be one " + modules, 1, modules.length);

            targetModuleID = modules[0];
            moduleID = targetModuleID.getModuleID();
            assertTrue("Ends with deployment-app.ear", moduleID.endsWith("deployment-app.ear"));

            ProgressObject operationProgress = manager.stop(modules);
            awaitCompletion(operationProgress, TIMEOUT);

            // Test getRunningModules
            modules = manager.getRunningModules(ModuleType.EAR, targets);
            assertEquals("after stopping deployment-app.ear, running EAR modules count expected to be zero" + modules, 0, modules.length);

            // Test getNonRunningModules
            modules = manager.getNonRunningModules(ModuleType.EAR, targets);
            assertEquals("after stopping deployment-app.ear, non-running EAR modules count expected to be one" + modules,1, modules.length);

            operationProgress = manager.start(modules);
            awaitCompletion(operationProgress, TIMEOUT);

            // Test getNonRunningModules
            modules = manager.getNonRunningModules(ModuleType.EAR, targets);
            assertEquals("after starting deployment-app.ear, non-running EAR modules count expected to be zero" + modules,0, modules.length);

            // Test getRunningModules
            modules = manager.getRunningModules(ModuleType.EAR, targets);
            assertEquals("after starting deployment-app.ear, running EAR modules count expected to be one" , 1, modules.length);

        } finally {
            jsr88Undeploy(manager, targetModules);
        }
    }

    @Test
    @Ignore("[AS7-2771] ModelControllerClient operations don't fail on invalid username/password")
    public void testListAvailableModulesUnauthorized() throws Exception {
        DeploymentManager manager = getDeploymentManager("nobody", "nopass");
        Target[] targets = manager.getTargets();
        try {
            manager.getAvailableModules(ModuleType.EAR, targets);
            fail("TargetException expected");
        } catch (TargetException ex) {
            // expected
        }
    }

    @Test
    public void testListAvailableModulesWrongHost() throws Exception {
        String uri = DeploymentManagerImpl.DEPLOYER_URI + "?targetType=as7&serverHost=wrongHost";
        DeploymentManager manager = getDeploymentManager(uri, Authentication.USERNAME, Authentication.PASSWORD);
        Target[] targets = manager.getTargets();
        try {
            manager.getAvailableModules(ModuleType.EAR, targets);
            fail("TargetException expected");
        } catch (TargetException ex) {
            // expected
        }
    }

    @Test
    public void testListAvailableModulesWrongPort() throws Exception {
        String uri = DeploymentManagerImpl.DEPLOYER_URI + "?targetType=as7&serverPort=9876";
        DeploymentManager manager = getDeploymentManager(uri, Authentication.USERNAME, Authentication.PASSWORD);
        Target[] targets = manager.getTargets();
        try {
            manager.getAvailableModules(ModuleType.EAR, targets);
            fail("TargetException expected");
        } catch (TargetException ex) {
            // expected
        }
    }

    private DeploymentManager getDeploymentManager() throws Exception {
        return getDeploymentManager(Authentication.USERNAME, Authentication.PASSWORD);
    }

    private DeploymentManager getDeploymentManager(String username, String password) throws Exception {
        String uri = DeploymentManagerImpl.DEPLOYER_URI + "?targetType=as7";
        return getDeploymentManager(uri, username, password);
    }

    private DeploymentManager getDeploymentManager(String uri, String username, String password) throws Exception {
        DeploymentFactoryImpl.register();
        DeploymentFactoryManager dfManager = DeploymentFactoryManager.getInstance();
        DeploymentFactory[] factories = dfManager.getDeploymentFactories();
        DeploymentManager deploymentManager = factories[0].getDeploymentManager(uri, username, password);
        return deploymentManager;
    }

    private ProgressObject jsr88Deploy(DeploymentManager manager, Archive<?> archive) throws Exception {
        Target[] targets = manager.getTargets();
        assertEquals(1, targets.length);

        InputStream deploymentPlan = createDeploymentPlan(archive.getName());

        // Deploy the test archive
        InputStream inputStream = archive.as(ZipExporter.class).exportAsInputStream();
        ProgressObject progress = manager.distribute(targets, inputStream, deploymentPlan);
        StateType state = awaitCompletion(progress, TIMEOUT);

        if (state == StateType.COMPLETED) {
            progress = manager.start(progress.getResultTargetModuleIDs());
            awaitCompletion(progress, TIMEOUT);
        }

        return progress;
    }

    private ProgressObject jsr88Undeploy(DeploymentManager manager, TargetModuleID[] resultTargetModuleIDs) throws Exception {
        Target[] targets = manager.getTargets();
        assertEquals(1, targets.length);

        ProgressObject progress = manager.stop(resultTargetModuleIDs);
        awaitCompletion(progress, TIMEOUT);

        progress = manager.undeploy(resultTargetModuleIDs);
        awaitCompletion(progress, TIMEOUT);

        return progress;
    }

    private StateType awaitCompletion(ProgressObject progress, long timeout) throws InterruptedException {
        final CountDownLatch latch = new CountDownLatch(1);
        progress.addProgressListener(new ProgressListener() {
            public void handleProgressEvent(ProgressEvent event) {
                DeploymentStatus status = event.getDeploymentStatus();
                if (status.isCompleted() || status.isFailed()) {
                    latch.countDown();
                }
            }
        });

        final DeploymentStatus status = progress.getDeploymentStatus();
        if (status.isCompleted())
            return status.getState();

        if (latch.await(timeout, TimeUnit.MILLISECONDS) == false)
            throw new IllegalStateException("Deployment timeout: " + progress);

        return status.getState();
    }

    private void assertServletAccess(String context) throws IOException {
        // Check that we can access the servlet
        URL servletURL = new URL("http://localhost:8080/" + context);
        BufferedReader br = new BufferedReader(new InputStreamReader(servletURL.openStream()));
        String message = br.readLine();
        assertEquals("Hello World!", message);
    }

    private InputStream createDeploymentPlan(String deploymentFile) throws Exception {

        String jbossDescriptorName = null;
        if (deploymentFile.endsWith(".war"))
            jbossDescriptorName = WAR_JBOSS_FILE;
        else if (deploymentFile.endsWith(".jar"))
            jbossDescriptorName = JAR_JBOSS_FILE;
        else if (deploymentFile.endsWith(".ear"))
            jbossDescriptorName = EAR_JBOSS_FILE;

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        JarOutputStream plan = new JarOutputStream(baos);

        URL descriptorURL = getClass().getClassLoader().getResource("jsr88/" + jbossDescriptorName);
        File jbossDescriptorFile = new File(descriptorURL.getPath());
        JarUtils.addJarEntry(plan, "!/" + jbossDescriptorName, new FileInputStream(jbossDescriptorFile));

        // Setup deployment plan meta data with propriatary descriptor
        DeploymentMetaData metaData = new DeploymentMetaData(deploymentFile);

        String[] strs = jbossDescriptorName.split("/");
        metaData.addEntry(deploymentFile, strs[strs.length - 1]);

        // Add the meta data to the deployment plan
        String metaStr = metaData.toXMLString();
        JarUtils.addJarEntry(plan, DeploymentMetaData.ENTRY_NAME, new ByteArrayInputStream(metaStr.getBytes()));

        plan.flush();
        plan.close();

        return new ByteArrayInputStream(baos.toByteArray());
    }

    private Archive<?> getWebArchive() {
        WebArchive archive = ShrinkWrap.create(WebArchive.class, "deployment-web.war");
        archive.addClasses(SampleServlet.class);
        archive.setWebXML("jsr88/WEB-INF/web.xml");
        return archive;
    }

    private Archive<?> getBadWebArchive() {
        WebArchive archive = ShrinkWrap.create(WebArchive.class, "deployment-bad-web.war");
        archive.addClasses(SampleServlet.class);
        archive.setWebXML("jsr88/WEB-INF/badweb.xml");
        return archive;
    }

    private Archive<?> getEjbArchive() {
        JavaArchive archive = ShrinkWrap.create(JavaArchive.class, "deployment-ejb.jar");
        archive.addClasses(Echo.class, EchoHome.class, EchoBean.class);
        archive.addAsManifestResource("jsr88/META-INF/ejb-jar.xml", "ejb-jar.xml");
        return archive;
    }

    private Archive<?> getEarArchive() {
        EnterpriseArchive archive = ShrinkWrap.create(EnterpriseArchive.class, "deployment-app.ear");
        archive.setApplicationXML("jsr88/META-INF/application.xml");
        archive.add(getWebArchive(), "/", ZipExporter.class);
        archive.add(getEjbArchive(), "/", ZipExporter.class);
        return archive;
    }
}
