/*
 * 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 in the distribution for a
 * full listing of individual contributors.
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License, v. 2.1.
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * 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,
 * v.2.1 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 *
 * (C) 2008,
 * @author JBoss Inc.
 */
package org.jboss.jbossts.qa.astests.ASCrashRecovery01;

import org.jboss.jbossts.qa.astests.taskdefs.ClientAction;
import org.jboss.jbossts.qa.astests.recovery.ASFailureSpec;
import org.jboss.jbossts.qa.astests.taskdefs.ASTestConfig;
import org.jboss.jbossts.qa.astests.taskdefs.TransactionLog;
import org.jboss.jbossts.qa.astests.crash.CrashRem;
import org.jboss.jbossts.qa.astests.crash.CrashRemHome;
import org.jboss.remoting.CannotConnectException;
import org.apache.tools.ant.BuildException;

import java.util.Map;
import java.io.*;

import javax.rmi.PortableRemoteObject;
import javax.transaction.*;
import javax.naming.NamingException;

public class Test03 implements ClientAction
{
    // the longest time to wait in millis before declaring a test a failed (overridable)
    private static final int MAX_TEST_TIME = 180000;

    private ASTestConfig config;
    private boolean isCMT = false;
    private boolean clientTx = false;
    private boolean isDebug = false;
    private boolean expectFailure = false;
    private int maxTestTime = MAX_TEST_TIME;

    private String storeDir = null;
    private String storeImple = "HashedActionStore";
    private String storeType = null; //"StateManager/BasicAction/TwoPhaseCoordinator/AtomicAction";
    private TransactionLog store;
    private int existingUids;

    private String name = "Test";
    private String serverName = "default";

    public boolean execute(ASTestConfig config, Map<String, String> params)
    {
        StringBuilder sb = new StringBuilder();
        ASFailureSpec[] fspecs = null;

        this.config = config;

        for (Map.Entry<String, String> me : params.entrySet())
        {
            String key = me.getKey().trim();
            String val = me.getValue().trim();

            if ("name".equals(key))
                name = val;
            else if ("cmt".equals(key))
                isCMT = val.equalsIgnoreCase("true");
            else if ("debug".equals(key))
                isDebug = val.equalsIgnoreCase("true");
            else if ("serverName".equals(key))
                serverName = val;
            else if ("storeType".equals(key))
                storeType = val;
            else if ("storeDir".equals(key))
                storeDir = val;
            else if ("clientTx".equals(key))
                clientTx = val.equalsIgnoreCase("true");
            else if ("storeImple".equals(key))
                storeImple = val;
            else if ("testTime".equals(key))
                maxTestTime = parseInt(val, "parameter testTime should represent a number of seconds: ");
            else if ("specs".equals(key))
                fspecs = parseSpecs(val, sb);
            else if ("wait".equals(key))
                suspendFor(Integer.parseInt(val));
        }

        sb.insert(0, ":\n").insert(0, name).insert(0, "Executing test ");

        System.out.println(sb);

        ClassLoader loader1 = Thread.currentThread().getContextClassLoader();
        ClassLoader loader2 = this.getClass().getClassLoader();

        try
        {
            String serverPath = config.getServerPath(serverName);

            // switch class loaders since a custom ant task runs with a different loader from the loader
            // that loader that loaded the class
            Thread.currentThread().setContextClassLoader(loader2);

            // get a handle to the transaction logs
            if (storeDir == null)
                storeDir = serverPath + "data/tx-object-store";
            else
                storeDir = serverPath + storeDir;

            store = new TransactionLog(storeDir, storeImple);

            if (expectFailure)
            {
                // this test may halt the VM so make sure the transaction log is empty
                // before starting the test - then the pass/fail check is simply to
                // test whether or not the log is empty (see recoverUids() below).
                try
                {
                    store.clearXids(storeType);
                }
                catch (Exception ignore)
                {
                }

                existingUids = getPendingUids();
            }

            // run the crash test
            return crashTest(fspecs);
        }
        catch (Exception e)
        {
            if (isDebug)
                e.printStackTrace();

            throw new BuildException(e);
        }
        finally
        {
            Thread.currentThread().setContextClassLoader(loader1);
        }
    }

    public boolean cancel() throws UnsupportedOperationException
    {
        throw new UnsupportedOperationException("TODO");
    }

    private ASFailureSpec[] parseSpecs(String specArg, StringBuilder sb)
    {
        ASFailureSpec[] fspecs = config.parseSpecs(specArg);

        for (ASFailureSpec spec : fspecs)
        {
            String name = (spec == null ? "INVALID" : spec.getName());

            if (spec != null && spec.willTerminateVM())
                expectFailure = true;

            sb.append("\t").append(name).append('\n');
        }

        return fspecs;
    }

    private int parseInt(String intValue, String errorMessage) throws IllegalArgumentException
    {
        try
        {
            return Integer.parseInt(intValue);
        }
        catch (NumberFormatException e)
        {
            System.out.println(errorMessage + e.getMessage());

            throw new IllegalArgumentException(e);
        }
    }

    // count how many pending transaction branches there are in the transaction log
    private int getPendingUids()
    {
        try
        {
            return store.getIds(storeType).size();
        }
        catch (Exception e)
        {
            e.printStackTrace();

            return -1;
        }
    }

    private CrashRem lookup(String name) throws Exception
    {
        Object o = config.getNamingContext(serverName).lookup(name);
        CrashRemHome home = (CrashRemHome) PortableRemoteObject.narrow(o, CrashRemHome.class);

        return home.create();
    }

    private UserTransaction startTx() throws NamingException, SystemException, NotSupportedException
    {
        UserTransaction tx = (UserTransaction) config.getNamingContext(serverName).lookup("UserTransaction");

        tx.begin();

        return tx;
    }

    private boolean crashTest(ASFailureSpec[] sa) throws Exception
    {
        UserTransaction tx = null;

        try
        {
            CrashRem cr = lookup(isCMT ? CrashRem.CMT_JNDI_NAME : CrashRem.BMT_JNDI_NAME);

            if (clientTx)
                tx = startTx();

            String res = cr.testXA(sa);

            return "Passed".equalsIgnoreCase(res);
        }
        catch (CannotConnectException e)
        {
            if (expectFailure)
            {
                print("Failure was expected: " + e.getMessage());

                return recoverUids();
            }
            else
            {
                System.err.println("XACrashTest:crashTest: Caught " + e);

                e.printStackTrace();
            }
        }
        catch (Throwable t)
        {
            t.printStackTrace();
            System.err.println("XACrashTest:crashTest: Caught " + t);
        }
        finally {
            if (clientTx)
                try
                {
                    tx.commit();
                }
                catch (Throwable e)
                {
                    System.out.println("User tx commit failure: " + e.getMessage());
                }
        }

        return false;
    }

    /**
     * Wait for any pending transactions to recover by restaring the AS.
     * @return true if all pending branches have been recovered
     * @throws IOException if the server cannot be started
     */
    private boolean recoverUids() throws IOException
    {
        int retryPeriod = 30000;
        int maxWait = maxTestTime;
        int pendingUids;

        do
        {
            pendingUids = getPendingUids();

            if (pendingUids == -1)
                return false;   // object store error

            if (pendingUids <= existingUids)
                return true;    // all uids recovered

            pendingUids -= existingUids;

            print("waiting for " + pendingUids + " branches");

            // wait for the server to start up the first time through
            if (maxWait == maxTestTime)
                config.startServer(serverName);

            suspendFor(retryPeriod);
            maxWait -= retryPeriod;
        } while (maxWait > 0);

        // the test failed to recover some uids - clear them out ready for the next test
        try
        {
            store.clearXids(storeType);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return false;
    }

    private void suspendFor(int millis)
    {
        try
        {
            Thread.sleep(millis);
        }
        catch (InterruptedException e)
        {
            System.out.println("Test " + name + " interupted");
        }
    }

    static void print(String msg)
    {
        System.out.println(msg);
    }
}
