/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.ode.test;

import org.apache.ode.bpel.common.evt.DebugBpelEventListener;
import org.apache.ode.bpel.dao.BpelDAOConnectionFactory;
import org.apache.ode.bpel.engine.BpelServerImpl;
import org.apache.ode.bpel.iapi.Message;
import org.apache.ode.bpel.iapi.MessageExchange;
import org.apache.ode.bpel.iapi.MessageExchange.Status;
import org.apache.ode.bpel.iapi.MyRoleMessageExchange;
import org.apache.ode.bpel.iapi.MyRoleMessageExchange.CorrelationStatus;
import org.apache.ode.bpel.iapi.ProcessStore;
import org.apache.ode.bpel.iapi.ProcessStoreEvent;
import org.apache.ode.bpel.iapi.ProcessStoreListener;
import org.apache.ode.bpel.memdao.BpelDAOConnectionFactoryImpl;
import org.apache.ode.dao.jpa.BPELDAOConnectionFactoryImpl;
import org.apache.ode.il.MockScheduler;
import org.apache.ode.il.config.OdeConfigProperties;
import org.apache.ode.store.ProcessConfImpl;
import org.apache.ode.store.ProcessStoreImpl;
import org.apache.ode.utils.DOMUtils;
import org.apache.ode.utils.GUID;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.w3c.dom.Element;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.xml.namespace.QName;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public abstract class BPELTestAbstract {
	private static final String SHOW_EVENTS_ON_CONSOLE = "no";

    protected BpelServerImpl _server;

    protected ProcessStore store;

    protected MessageExchangeContextImpl mexContext;

    protected EntityManager em;

    protected EntityManagerFactory emf;

    protected MockScheduler scheduler;

    protected BpelDAOConnectionFactory _cf;

    /** Failures that have been detected. */
    protected List<Failure> _failures;

    /** The things we'd like to deploy. */
    protected List<Deployment> _deployments;

    /** The things we'd like to invoke. */
    protected List<Invocation> _invocations;

    /** What's actually been deployed. */
    private List<Deployment> _deployed;

    @Before
    public void setUp() throws Exception {
        _failures = new CopyOnWriteArrayList<Failure>();
        _server = new BpelServerImpl();
        mexContext = new MessageExchangeContextImpl();
        _deployments = new ArrayList<Deployment>();
        _invocations = new ArrayList<Invocation>();
        _deployed = new ArrayList<Deployment>();

        if (Boolean.getBoolean("org.apache.ode.test.persistent")) {
            emf = Persistence.createEntityManagerFactory("ode-unit-test-embedded");
            em = emf.createEntityManager();
            _cf = new BPELDAOConnectionFactoryImpl();
            _server.setDaoConnectionFactory(_cf);
            scheduler = new MockScheduler() {
                @Override
                public void begin() {
                    super.begin();
                    em.getTransaction().begin();
                }

                @Override
                public void commit() {
                    super.commit();
                    em.getTransaction().commit();
                }

                @Override
                public void rollback() {
                    super.rollback();
                    em.getTransaction().rollback();
                }

            };
        } else {
            scheduler = new MockScheduler();
            _cf = new BpelDAOConnectionFactoryImpl(scheduler);
            _server.setDaoConnectionFactory(_cf);
        }
        _server.setInMemDaoConnectionFactory(new BpelDAOConnectionFactoryImpl(scheduler));
        _server.setScheduler(scheduler);
        _server.setBindingContext(new BindingContextImpl());
        _server.setMessageExchangeContext(mexContext);
        scheduler.setJobProcessor(_server);
        store = new ProcessStoreImpl(null, null, "jpa", new OdeConfigProperties(new Properties(), ""), true);
        store.registerListener(new ProcessStoreListener() {
            public void onProcessStoreEvent(ProcessStoreEvent event) {
                // bounce the process
                _server.unregister(event.pid);
                if (event.type != ProcessStoreEvent.Type.UNDEPLOYED) {
                    ProcessConfImpl conf = (ProcessConfImpl) store.getProcessConfiguration(event.pid);
                    // Test processes always run with in-mem DAOs
                    conf.setTransient(true);
                    _server.register(conf);
                }
            }
        });
        _server.setConfigProperties(getConfigProperties());
        _server.registerBpelEventListener(new DebugBpelEventListener());
        _server.init();
        _server.start();
    }

    @After
    public void tearDown() throws Exception {
        for (Deployment d : _deployed) {
            try {
                store.undeploy(d.deployDir);
            } catch (Exception ex) {
                ex.printStackTrace();
                System.err.println("Error undeploying " + d);
            }
        }

        if (em != null) em.close();
        if (emf != null) emf.close();

        _server.stop();
        _failures = null;
        _deployed = null;
        _deployments = null;
        _invocations = null;

    }

    protected void negative(String deployDir) throws Throwable {
        try {
            go(new File(deployDir));
        } catch (junit.framework.AssertionFailedError ex) {
            return;
        }
        Assert.fail("Expecting test to fail");
    }

    protected void go(String deployDir) throws Exception {
        go(makeDeployDir(deployDir));
    }

    protected Deployment addDeployment(String deployDir) {
        return addDeployment(makeDeployDir(deployDir));
    }

    protected Deployment addDeployment(File deployDir) {
        Deployment deployment = new Deployment(deployDir);
        _deployments.add(deployment);
        return deployment;
    }

    protected void go(File deployDir) throws Exception {
        setup(deployDir);
        go();
    }

    protected void setup(File deployDir) throws Exception {
        addDeployment(deployDir);
        int propsFileCnt = 0;
        File testPropsFile = new File(deployDir, "test.properties");
        if (!testPropsFile.exists()) {
            propsFileCnt++;
            testPropsFile = new File(deployDir, "test" + propsFileCnt + ".properties");
            if (!testPropsFile.exists()) {
                System.err.println("can't find " + testPropsFile);
            }
        }

        if (!testPropsFile.exists()) {
            Assert.fail("Test property file not found in " + deployDir);
        }

        while (testPropsFile.exists()) {
            Properties testProps = new Properties();
            InputStream is = new FileInputStream(testPropsFile);
            try {
                testProps.load(is);
            } finally {
                is.close();
            }

            final QName serviceId = new QName(testProps.getProperty("namespace"), testProps.getProperty("service"));
            final String operation = testProps.getProperty("operation");

            for (int i = 1; testProps.getProperty("request" + i) != null; i++) {
                final String in = testProps.getProperty("request" + i);
                final String responsePattern = testProps.getProperty("response" + i);

                addInvoke(testPropsFile + "#" + i, serviceId, operation, in, responsePattern);
            }
            propsFileCnt++;
            testPropsFile = new File(deployDir, "test" + propsFileCnt + ".properties");
        }
    }

    protected Invocation addInvoke(String id, QName target, String operation, String request, String responsePattern) throws Exception {

        Invocation inv = new Invocation(id);
        inv.target = target;
        inv.operation = operation;
        inv.request = DOMUtils.stringToDOM(request);
        inv.expectedStatus = null;
        if (responsePattern != null) {
            inv.expectedFinalStatus = MessageExchange.Status.RESPONSE;
            inv.expectedResponsePattern = Pattern.compile(responsePattern, Pattern.DOTALL);
        }

        _invocations.add(inv);
        return inv;
    }

    protected void go() throws Exception {
        try {
            doDeployments();
            doInvokes();
        } finally {
            checkFailure();
        }
    }

    protected void checkFailure() {
        StringBuffer sb = new StringBuffer("Failure report:\n");
        for (Failure failure : _failures) {
            sb.append(failure);
            sb.append('\n');
        }
        if (_failures.size() != 0) {
            System.err.println(sb.toString());
            Assert.fail(sb.toString());
        }
    }


    protected Deployment deploy(String location) {
        Deployment deployment = new Deployment(makeDeployDir(location));
        doDeployment(deployment);
        return deployment;
    }

    protected void doDeployments() {
        for (Deployment d : _deployments)
            doDeployment(d);
    }

    /**
     * Do all the registered deployments.
     *
     * @param d
     */
    protected void doDeployment(Deployment d) {
        Collection<QName> procs;

        try {
            procs = store.deploy(d.deployDir);

            _deployed.add(d);
        } catch (Exception ex) {
            if (d.expectedException == null) {
                ex.printStackTrace();
                failure(d, "DEPLOY: Unexpected exception: " + ex, ex);
            } else if (!d.expectedException.isAssignableFrom(ex.getClass())) {
                ex.printStackTrace();
                failure(d, "DEPLOY: Wrong exception; expected " + d.expectedException + " but got " + ex.getClass(), ex);
            }


            return;
        }

        try {
            for (QName procName : procs) {
                ProcessConfImpl conf = (ProcessConfImpl) store.getProcessConfiguration(procName);
                // Test processes always run with in-mem DAOs
                conf.setTransient(true);
                _server.register(conf);
            }
        } catch (Exception ex) {
            if (d.expectedException == null)
                failure(d, "REGISTER: Unexpected exception: " + ex, ex);
            else if (!d.expectedException.isAssignableFrom(ex.getClass()))
                failure(d, "REGISTER: Wrong exception; expected " + d.expectedException + " but got " + ex.getClass(), ex);
        }
    }

    protected void doUndeployments() {
        for (Deployment d : _deployments) {
            try {
                undeploy(d);
            } catch (Exception ex) {
                ex.printStackTrace();
                failure(d, "Undeployment failed.", ex);
            }
        }

        _deployments.clear();
    }

    protected void undeploy(Deployment d) {
        if (_deployed.contains(d)) {
            _deployed.remove(d);
            store.undeploy(d.deployDir);
        }
    }
    protected void doInvokes() throws Exception {
        ArrayList<Thread> testThreads = new ArrayList<Thread>();
        for (Invocation i : _invocations) {
            InvokerThread t = new InvokerThread(i);
            testThreads.add(t);
        }

        for (Thread testThread : testThreads) {
            testThread.start();
            if (testThreads.size() > 0) Thread.sleep(2000);
        }

        for (Thread testThread : testThreads)
            testThread.join();

    }

    private void failure(Object where) {
        failure(where, "Failure", null);
    }

    private void failure(Object where, String message, Exception ex) {
        Failure f = new Failure(where, message, ex);
        _failures.add(f);
        Assert.fail(f.toString());
    }

    private void failure(Object where, String message, Object expected, Object actual) {
        Failure f = new Failure(where, message, expected, actual, null);
        _failures.add(f);
        Assert.fail(f.toString());
    }

    protected boolean isFailed() {
        return !_failures.isEmpty();
    }

    protected File makeDeployDir(String deployDir) {
        String deployxml = deployDir + "/deploy.xml";
        URL deployxmlurl = getClass().getResource(deployxml);
        if (deployxmlurl == null) {
            Assert.fail("Resource not found: " + deployxml);
        }
        try {
            return new File(deployxmlurl.toURI().getPath()).getParentFile();
        } catch (URISyntaxException e) {
            e.printStackTrace();
            Assert.fail(e.getMessage());
            return null;
        }
    }

    /**
     * Override this to provide configuration properties for Ode extensions 
     * like BpelEventListeners.
     *
     * @return
     */
    protected Properties getConfigProperties() {
        // could also return null, returning an empty properties
        // object is more fail-safe.
        Properties p = new Properties();
        p.setProperty("debugeventlistener.dumpToStdOut", SHOW_EVENTS_ON_CONSOLE);
        return p;
    }

    protected static class Failure {
        Object where;

        String msg;

        Object expected;

        Object actual;

        Exception ex;

        public Failure(Object where, String msg, Exception ex) {
            this(where, msg, null, null, ex);
        }

        public Failure(Object where, String msg, Object expected, Object actual, Exception ex) {
            this.actual = actual;
            this.expected = expected;
            this.where = where;
            this.msg = msg;
            this.ex = ex;
        }

        public String toString() {
            StringBuffer sbuf = new StringBuffer(where + ": " + msg);
            if (ex != null) {
                sbuf.append("; got exception msg: " + ex.getMessage());
            }
            if (actual != null)
                sbuf.append("; got " + actual + ", expected " + expected);
            return sbuf.toString();
        }
    }

    /**
     * Represents a test deployement.
     *
     * @author mszefler
     *
     */
    public static class Deployment {
        /** The directory containing the deploy.xml and artefacts. */
        public File deployDir;

        /** If non-null the type of exception we expect to get when we deploy. */
        public Class expectedException = null;

        public Deployment(File deployDir) {
            this.deployDir = deployDir;
        }

        public String toString() {
            return "Deployment#" + deployDir;
        }
    }

    /**
     * Represents an test invocation of the BPEL engine.
     *
     * @author mszefler
     */
    public static class Invocation {
        /** Identifier (for reporting). */
        public String id;

        /** Name of the operation to invoke. */
        public String operation;

        /** Name of service to invoke. */
        public QName target;

        /** Expected RegExp pattern for the response, or null */
        public Pattern expectedResponsePattern;

        /** The request message that should be sent to the server */
        public Element request;

        /** Number of ms to wait (relative to other invokes) before invoking. */
        public long invokeDelayMs = 0L;

        /** If non-null, expect an exception of this class (or subclass) on invoke. */
        public Class expectedInvokeException = null;

        /** If non-null, expecte this status right after invoke. */
        public MessageExchange.Status expectedStatus = null;

        /** If non-null, expect this status after response received. */
        public MessageExchange.Status expectedFinalStatus = MessageExchange.Status.COMPLETED_OK;

        /** If non-null, expect this correlation status right after invoke. */
        public CorrelationStatus expectedCorrelationStatus = null;

        /** If non-null, expect this correlation after response received. */
        public CorrelationStatus expectedFinalCorrelationStatus = null;

        /** Maximum number of ms to wait for a response. */
        public long maximumWaitMs = 60 * 1000;

        /** If non-null, minimum number of ms before a response should be available. */
        public Long minimumWaitMs = null;

        long invokeTime;

        Exception invokeException;

        QName requestType;

        public Invocation(String id) {
            this.id = id;
        }

        public String toString() {
            return "Invocation#" + id;
        }

    }

    class InvokerThread extends Thread {
        Invocation _invocation;

        InvokerThread(Invocation invocation) {
            _invocation = invocation;
        }

        public void run() {
            final MyRoleMessageExchange mex;
            final Future<MessageExchange.Status> running;

            // Wait for it....
            try {
                Thread.sleep(_invocation.invokeDelayMs);
            } catch (Exception ex) {
            }

            scheduler.begin();
            try {
                mex = _server.getEngine().createMessageExchange(new GUID().toString(), _invocation.target, _invocation.operation);
                mexContext.clearCurrentResponse();

                Message request = mex.createMessage(_invocation.requestType);
                request.setMessage(_invocation.request);
                _invocation.invokeTime = System.currentTimeMillis();
                running = mex.invoke(request);

                Status status = mex.getStatus();
                CorrelationStatus cstatus = mex.getCorrelationStatus();
                if (_invocation.expectedStatus != null && !status.equals(_invocation.expectedStatus))
                    failure(_invocation, "Unexpected message exchange status", _invocation.expectedStatus, status);

                if (_invocation.expectedCorrelationStatus != null && !cstatus.equals(_invocation.expectedCorrelationStatus))
                    failure(_invocation, "Unexpected correlation status", _invocation.expectedCorrelationStatus, cstatus);

            } catch (Exception ex) {
                if (_invocation.expectedInvokeException == null)
                    failure(_invocation, "Unexpected invocation exception.", ex);
                else if (_invocation.expectedInvokeException.isAssignableFrom(ex.getClass()))
                    failure(_invocation, "Unexpected invocation exception.", _invocation.expectedInvokeException, ex.getClass());

                return;
            } finally {
                scheduler.commit();
            }

            if (isFailed())
                return;

            try {
                running.get(_invocation.maximumWaitMs, TimeUnit.MILLISECONDS);
            } catch (Exception ex) {
                failure(_invocation, "Exception on future object.", ex);
                return;
            }

            long ctime = System.currentTimeMillis();
            long itime = ctime - _invocation.invokeTime;
            if (_invocation.minimumWaitMs != null && _invocation.minimumWaitMs >= itime)
                failure(_invocation, "Response received too soon.", _invocation.minimumWaitMs, itime);

            if (_invocation.maximumWaitMs <= itime)
                failure(_invocation, "Response took too long.", _invocation.maximumWaitMs, itime);

            if (isFailed())
                return;

            if (_invocation.expectedResponsePattern != null) {
                scheduler.begin();
                try {
                    Status finalstat = mex.getStatus();
                    if (_invocation.expectedFinalStatus != null && !_invocation.expectedFinalStatus.equals(finalstat))
                        if (finalstat.equals(Status.FAULT)) {
                            failure(_invocation, "Unexpected final message exchange status", _invocation.expectedFinalStatus, "FAULT: "
                                    + mex.getFault() + " | " + mex.getFaultExplanation());
                        } else {
                            failure(_invocation, "Unexpected final message exchange status", _invocation.expectedFinalStatus, finalstat);
                        }

                    if (_invocation.expectedFinalCorrelationStatus != null
                            && !_invocation.expectedFinalCorrelationStatus.equals(mex.getCorrelationStatus())) {
                        failure(_invocation, "Unexpected final correlation status", _invocation.expectedFinalCorrelationStatus, mex
                                .getCorrelationStatus());
                    }
                    if (mex.getResponse() == null)
                        failure(_invocation, "Expected response, but got none.", null);
                    String responseStr = DOMUtils.domToString(mex.getResponse().getMessage());
                    System.out.println("=>" + responseStr);
                    Matcher matcher = _invocation.expectedResponsePattern.matcher(responseStr);
                    if (!matcher.matches())
                        failure(_invocation, "Response does not match expected pattern", _invocation.expectedResponsePattern, responseStr);
                } finally {
                    scheduler.commit();
                }
            }
        }
    }
}
