/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2008 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 * 
 * This library 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 any later version.
 * 
 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas.jtests.clients.versioning;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Set;

import javax.management.Attribute;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import junit.framework.Test;
import junit.framework.TestSuite;

import org.apache.commons.codec.binary.Base64;
import org.objectweb.jonas.jtests.util.JTestCase;

/**
 * Versioning Service test suite. Some fields are static since we need to keep
 * those information until end of test.
 * 
 * @author S. Ali Tokmen
 */
public class F_Versioning extends JTestCase {

    /**
     * JNDI entry to look for.
     */
    private static final String EJB_NAME =
        "org.ow2.jonas.test.versioning.TestBeanImpl_org.ow2.jonas.test.versioning.TestBean@Local";

    /**
     * Has this been initialized?
     */
    private static boolean initialized = false;

    /**
     * J2EEServer object name.
     */
    private static ObjectName j2eeServer;

    /**
     * Is the deployment service in development mode?
     */
    private static boolean developmentMode = false;

    /**
     * Versioning service object name.
     */
    private static ObjectName versioning;

    /**
     * HTTP port number.
     */
    private static Integer httpPort;

    /**
     * Was versioning started at the beginning of the test?
     */
    private static boolean versioningStarted;

    /**
     * Old default deployment policy if versioning was started at the beginning
     * of the test.
     */
    private static String defaultDeploymentPolicy;

    /**
     * File name of the version 1.0.0.
     */
    private static String version1File;

    /**
     * File name of the version 2.0.0.
     */
    private static String version2File;

    /**
     * File name of the secured-war.
     */
    private static String securedWarFile;

    /**
     * File name of the non-versioned-war.
     */
    private static String nonVersionedWarFile;

    /**
     * Cookie manager that will be used as soon as version 1.0.0 gets deployed.
     * It is expected to stay on version 1.0.0.
     */
    private static CookieManager client1;

    /**
     * Cookie manager that will be used as soon as version 2.0.0 gets deployed.
     * It is expected to stay on version 1.0.0.
     */
    private static CookieManager client2;

    /**
     * Cookie manager that will be used as soon as version 2.0.0 gets set as
     * default version. It is expected to stay on version 2.0.0.
     */
    private static CookieManager client3;

    /**
     * Constructor
     */
    public F_Versioning(String name) {
        super(name);
    }

    /**
     * init environment
     */
    @SuppressWarnings("unchecked")
    protected void setUp() {
        super.setUp();

        if (!initialized) {
            try {
                j2eeServer = ObjectName.getInstance(jonasName + ":j2eeType=J2EEServer,name=" + jonasName);

                // Get the HTTP service port
                ObjectName httpServer = ObjectName.getInstance(jonasName + ":type=Connector,*");
                for (ObjectInstance instance : (Set<ObjectInstance>) cnx.queryMBeans(httpServer, null)) {
                    String protocol = (String) cnx.getAttribute(instance.getObjectName(), "protocol");
                    if (protocol.startsWith("HTTP")) {
                        httpPort = (Integer) cnx.getAttribute(instance.getObjectName(), "port");
                        break;
                    }
                }
                if (httpPort == null) {
                    fail("Cannot find any running HTTP service!");
                }

                // Get if deployment manager is in development mode
                ObjectName deploymentMonitor = ObjectName.getInstance(jonasName + ":type=service,name=depmonitor");
                developmentMode = ((Boolean) cnx.getAttribute(deploymentMonitor, "developmentMode")).booleanValue();

                // Create all cookie managers
                client1 = new CookieManager();
                client2 = new CookieManager();
                client3 = new CookieManager();
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }

        initialized = true;
    }

    /**
     * This suite is all test cases
     */
    public static Test suite() {
        return new TestSuite(F_Versioning.class);
    }

    public static void main(String args[]) {
        String testtorun = null;
        // Get args
        for (int argn = 0; argn < args.length; argn++) {
            String s_arg = args[argn];

            if (s_arg.equals("-n")) {
                testtorun = args[++argn];
            }
        }
        if (testtorun == null) {
            junit.textui.TestRunner.run(suite());
        } else {
            junit.textui.TestRunner.run(new F_Versioning(testtorun));
        }
    }

    @SuppressWarnings("unchecked")
    public void testEnableVersioning() throws Exception {
        final Object[] opParams = {"versioning"};
        final String[] opSignature = {"java.lang.String"};

        if ("RUNNING".equals(cnx.invoke(j2eeServer, "getServiceState", opParams, opSignature))) {
            versioningStarted = true;
            defaultDeploymentPolicy = (String) cnx.getAttribute(versioning, "DefaultDeploymentPolicy");
            cnx.invoke(j2eeServer, "stopService", opParams, opSignature);
        }

        cnx.invoke(j2eeServer, "startService", opParams, opSignature);
        while (!"RUNNING".equals(cnx.invoke(j2eeServer, "getServiceState", opParams, opSignature))) {
            System.out.println("versioning service not RUNNING yet...");
            Thread.sleep(1000);
        }

        // Get the Versioning service
        ObjectName service = ObjectName.getInstance(jonasName + ":type=service,*");
        for (ObjectInstance instance : (Set<ObjectInstance>) cnx.queryMBeans(service, null)) {
            String name = instance.getObjectName().getKeyProperty("name");
            if ("versioning".equals(name)) {
                versioning = instance.getObjectName();
                break;
            }
        }
        if (versioning == null) {
            fail("Cannot find the versioning service!");
        }

        boolean versioningEnabled = ((Boolean) cnx.getAttribute(versioning, "VersioningEnabled")).booleanValue();
        if (!versioningEnabled) {
            fail("Cannot enable the versioning service");
        }
    }

    public void testDeployVersion1() throws Exception {
        version1File = deployApplication("conformance/resources/test-versioning/version1.0.0/test-versioning.ear");

        verifyVersion(client1, "1.0.0");
    }

    public void testDeployVersion2() throws Exception {
        version2File = deployApplication("conformance/resources/test-versioning/version2.0.0/test-versioning.ear");

        verifyVersion(client1, "1.0.0");
        verifyVersion(client2, "1.0.0");
    }

    public void testSetDefaultVersion2() throws Exception {
        final Object[] opParams = {"/test-versioning-version2.0.0", "Default"};
        final String[] opSignature = {"java.lang.String", "java.lang.String"};

        ObjectName webModule = ObjectName.getInstance(jonasName
            + ":j2eeType=WebModule,name=//localhost/test-versioning,J2EEApplication=none,J2EEServer=" + jonasName
            + ",virtualContext=true");
        cnx.invoke(webModule, "rebindContext", opParams, opSignature);

        verifyVersion(client1, "1.0.0");
        verifyVersion(client2, "1.0.0");
        verifyVersion(client3, "2.0.0");
    }

    public void testUndeployVersion1() throws Exception {
        undeployApplication(version1File);

        verifyVersion(client1, "2.0.0");
        verifyVersion(client2, "2.0.0");
        verifyVersion(client3, "2.0.0");
    }

    public void testDefaultEJBBindingGarbageCollection() throws Exception {
        Context ctx = new InitialContext();
        try {
            ctx.lookupLink(F_Versioning.EJB_NAME);
            fail("JNDI reference did not get garbage collected by the versioning service!");
        } catch (NamingException expected) {
            // This is expected
        }
    }

    public void testUndeployVersion2() throws Exception {
        undeployApplication(version2File);

        verifyVersion(client1, null);
        verifyVersion(client2, null);
        verifyVersion(client3, null);
    }

    public void testDeploySecuredWar() throws Exception {
        securedWarFile = deployApplication("conformance/resources/test-versioning/secured-war/secured-war.war");
    }

    public void testSecuredWarWithoutCredentials() throws Exception {
        String response = getURL("http://localhost:" + httpPort + "/secured-war/", null, null);
        checkString(response, "Connection error: Unauthorized", "");
    }

    public void testSecuredWarWithIncorrectCredentials() throws Exception {
        String response = getURL("http://localhost:" + httpPort + "/secured-war/", "jetty", "jetty");
        checkString(response, "Connection error: Forbidden", "");
    }

    public void testSecuredWarWithCorrectCredentials() throws Exception {
        String response = getURL("http://localhost:" + httpPort + "/secured-war/", "jonas", "jonas");
        checkString(response, "Hello, World", "");
    }

    public void testDeployNonVersionedWar() throws Exception {
        nonVersionedWarFile = deployApplication("conformance/resources/test-versioning/non-versioned-war/non-versioned-war.war");
    }

    public void testNonVersionedWar() throws Exception {
        String response = getURL("http://localhost:" + httpPort + "/secured-war/non-versioned/", null, null);
        checkString(response, "Hello again, World", "");
    }

    public void testUndeploySecuredWar() throws Exception {
        undeployApplication(securedWarFile);

        String response = getURL("http://localhost:" + httpPort + "/secured-war/", null, null);
        checkString(response, "Connection error: Not Found", "");
    }

    public void testNonVersionedWarAgain() throws Exception {
        String response = getURL("http://localhost:" + httpPort + "/secured-war/non-versioned/", null, null);
        checkString(response, "Hello again, World", "");
    }

    public void testUndeployNonVersionedWar() throws Exception {
        undeployApplication(nonVersionedWarFile);

        String response = getURL("http://localhost:" + httpPort + "/secured-war/non-versioned/", null, null);
        checkString(response, "Connection error: Not Found", "");
    }

    public void testDisableVersioning() throws Exception {
        final Object[] opParams = {"versioning"};
        final String[] opSignature = {"java.lang.String"};
        cnx.invoke(j2eeServer, "stopService", opParams, opSignature);
        while ("RUNNING".equals(cnx.invoke(j2eeServer, "getServiceState", opParams, opSignature))) {
            System.out.println("versioning service still RUNNING...");
            Thread.sleep(1000);
        }
        if (versioningStarted) {
            versioningStarted = true;
            cnx.invoke(j2eeServer, "startService", opParams, opSignature);
            cnx.setAttribute(versioning, new Attribute("DefaultDeploymentPolicy", defaultDeploymentPolicy));
        }
    }

    private String deployApplication(final String filename) throws Exception {
        Object[] opParams;
        String[] opSignature;

        File file = new File(filename);
        System.out.println("Now deploying " + file.getAbsolutePath());
        FileInputStream inputStream = new FileInputStream(file);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buf = new byte[2048];
        // Read bytes
        int len;
        while ((len = inputStream.read(buf)) > 0) {
            baos.write(buf, 0, len);
        }
        byte[] bytesOfFile = baos.toByteArray();

        opParams = new Object[] {bytesOfFile, file.getName(), false};
        opSignature = new String[] {"[B", "java.lang.String", "boolean"};

        String sendFile = (String) cnx.invoke(j2eeServer, "sendFile", opParams, opSignature);

        if (developmentMode) {
            // Development mode: file should get deployed automatically
            final int timeout = 60;
            opParams = new Object[] {sendFile};
            opSignature = new String[] {"java.lang.String"};
            // Wait up to timeout seconds for deployment
            for (int i = 0; i < timeout; i++) {
                Thread.sleep(1000);
                boolean isDeployed = false;
                try {
                    isDeployed = ((Boolean) cnx.invoke(j2eeServer, "isDeployed", opParams, opSignature)).booleanValue();
                } catch (Exception e) {
                    // Ignored
                }
                if (isDeployed) {
                    // Deployed :)
                    return sendFile;
                }
                System.out.println("Waiting for \"" + sendFile + "\" to get auto-deployed...");
            }
            System.out.println("Auto-deployment timed out! Will now try manual deployment.");
        }

        // We reach this point only if development mode not active or auto
        // deployment timed out. Try to deploy, if it fails we'll have a proper
        // stack trace.
        opParams = new Object[] {sendFile};
        opSignature = new String[] {"java.lang.String"};

        cnx.invoke(j2eeServer, "deploy", opParams, opSignature);

        return sendFile;
    }

    private void undeployApplication(final String filename) throws Exception {
        Object[] opParams;
        String[] opSignature;

        opParams = new Object[] {filename};
        opSignature = new String[] {"java.lang.String"};
        cnx.invoke(j2eeServer, "undeploy", opParams, opSignature);
        cnx.invoke(j2eeServer, "removeModuleFile", opParams, opSignature);
    }

    private void verifyVersion(final CookieManager cookies, final String version) throws Exception {
        URL url = new URL("http://localhost:" + httpPort + "/test-versioning/");
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        System.out.println("Sending cookies: " + cookies);
        cookies.setCookies(connection);
        if (version == null) {
            try {
                connection.getInputStream();
            } catch (Exception ignored) {
                // We'll check the response code only
            }
            if (connection.getResponseCode() != 404) {
                fail("Got a valid response [" + connection.getResponseCode() + "] even after application undeployment!");
            }
        } else {
            InputStream inputStream;
            try {
                inputStream = connection.getInputStream();
            } catch (Exception e) {
                fail("HTTP request failed: " + connection.getResponseCode() + " " + connection.getResponseMessage());
                return;
            }
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            StringBuilder builder = new StringBuilder();

            String inputLine;
            while ((inputLine = reader.readLine()) != null) {
                builder.append(inputLine);
                builder.append(' ');
            }
            reader.close();

            String response = builder.toString();
            checkString(response, "WAR version", ": " + version);
            checkString(response, "EJB version", ": " + version);
        }
        cookies.storeCookies(connection);
        System.out.println("Received cookies: " + cookies);
    }

    private String getURL(final String url, final String username, final String password) throws Exception {
        HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
        con.setRequestMethod("GET");

        if (username != null) {
            Base64 enc = new Base64();
            String authData = username + ":" + password;
            byte[] base64AuthData = enc.encode(authData.getBytes());
            con.setRequestProperty("Authorization", "Basic " + new String(base64AuthData, "UTF-8"));
        }

        String response = null;
        int responseCode = con.getResponseCode();

        if (responseCode == HttpURLConnection.HTTP_OK) {
            InputStream is = con.getInputStream();
            byte[] b = new byte[is.available()];
            int nbOctetsLus = is.read(b);
            response = new String(b, 0, nbOctetsLus);
        } else {
            response = "Connection error: " + con.getResponseMessage();
        }

        return response;
    }

    private void checkString(String haystack, String start, String end) {
        String needle = start + end;
        if (!haystack.contains(needle)) {
            fail(start + " doesn't match \"" + needle + "\", response was: \"" + haystack + "\"");
        }
    }
}
