package org.jacorb.test.harness;

import junit.framework.AssertionFailedError;
import org.junit.experimental.categories.Category;
import org.omg.Messaging.ExceptionHolder;

/**
 * A special <code>ClientServerTestCase</code> for testing asynchronous
 * method invocations following the callback model.
 * <p>
 * This class makes it convenient to issue an asynchronous invocation
 * and wait for the reply within the same test case.  There is an abstract
 * <code>ReplyHandler</code> defined as an inner class of this class.
 * It provides a synchronization mechanism so that the main thread of
 * control that executes the test case can wait on the reply handler
 * until it has received and analyzed the reply.
 * <p>
 * As an example, assume that you want to test asynchronous invocations
 * of an interface <code>MyServer</code>.  You would create a subclass
 * of <code>CallbackTestCase</code>, and extend the generic
 * <code>ReplyHandler</code> for <code>MyServer</code> as follows:
 *
 * <p><blockquote><pre>
 * public class MyServerTest extends CallbackTestCase
 * {
 *     private ReplyHandler extends CallbackTestCase.ReplyHandler
 *                          implements MyServerOperations
 *     {
 *         // ...
 *     }
 *
 * }
 * </pre></blockquote><p>
 *
 * Each method from <code>MyServerOperations</code> should be
 * implemented to call <code>wrong_reply()</code> (for reply methods) or
 * <code>wrong_exception()</code> (for <code>_excep</code> methods).  In
 * the actual test cases, these methods will in turn be overwritten
 * anonymously for the correct replies.
 * <p>
 * Your type-specific <code>ReplyHandler</code> will be turned into a
 * CORBA object using the tie approach.  This is best done in a
 * convenience method such as the following:
 *
 * <p><blockquote><pre>
 * private AMI_MyServerHandler ref ( ReplyHandler handler )
 * {
 *     AMI_MyServerHandlerPOATie tie =
 *         new AMI_MyServerHandlerPOATie( handler )
 *         {
 *             public org.omg.CORBA.portable.OutputStream
 *                 _invoke( String method,
 *                          org.omg.CORBA.portable.InputStream _input,
 *                          org.omg.CORBA.portable.ResponseHandler handler )
 *                 throws org.omg.CORBA.SystemException
 *             {
 *                 try
 *                 {
 *                     return super._invoke( method, _input, handler );
 *                 }
 *                 catch( AssertionFailedError e )
 *                 {
 *                     return null;
 *                 }
 *             }
 *         };
 *     return tie._this( setup.getClientOrb() );
 * }
 * </pre></blockquote><p>
 *
 * The <code>_invoke()</code> method of the POATie is overridden anonymously
 * here to catch JUnit's <code>AssertionFailedError</code>s.  Without this,
 * a failed test case would cause all subsequent test cases to fail as well.
 * You can copy this method verbatim into your own source, except for
 * changing the name of the AMI classes.
 * <p>
 * An individual test case may then look like this:
 *
 * <p><blockquote><pre>
 * public void test_some_operation()
 * {
 *     ReplyHandler handler = new ReplyHandler()
 *     {
 *         public void some_operation( int ami_return_val )
 *         {
 *             assertEquals( 123, ami_return_val );
 *             pass();
 *         }
 *     };
 *     ( ( _CallbackServerStub ) server )
 *              .sendc_some_operation( ref( handler ) );
 *     handler.wait_for_reply( 200 );
 * }
 * </pre></blockquote><p>
 *
 * The <code>ReplyHandler</code> is extended anonymously to handle and
 * analyze the expected reply.  You may use any of the
 * <code>assert...()</code> methods defined in
 * <code>org.junit.Assert</code> to do this (they are redefined in
 * the generic <code>ReplyHandler</code>).  Unlike in a normal test case,
 * you must also call the <code>pass()</code> method explicitly if the
 * test case did not fail.  After the <code>ReplyHandler</code> has been
 * defined, the asynchronous operation is invoked using the corresponding
 * <code>sendc_</code> method.  The calling thread then waits for a reply
 * for at most 200 milliseconds.  If no reply is received within that
 * time, the test case fails.
 *
 * @author Andre Spiegel &lt;spiegel@gnu.org&gt;
 */
@Category(IMRExcludedClientServerCategory.class)
public class CallbackTestCase extends ClientServerTestCase
{
    protected abstract class ReplyHandler
    {
        private boolean replyReceived  = false;
        private boolean testFailed     = false;
        private String  failureMessage = null;

        protected ReplyHandler() {
            super();
        }

        public synchronized void wait_for_reply(long timeout)
        {
            try
            {
                long start = System.currentTimeMillis();

                while(!replyReceived && System.currentTimeMillis() < start + timeout)
                {
                    this.wait(timeout);
                }

                if ( !replyReceived )
                {
                        org.junit.Assert.fail
                            ( "no reply within timeout ("
                              + timeout + "ms)" );
                }

                if ( testFailed )
                {
                    org.junit.Assert.fail( failureMessage );
                }
            }
            catch ( InterruptedException e )
            {
                org.junit.Assert.fail
                    ( "interrupted while waiting for reply" );
            }
            finally
            {
                replyReceived  = false;
                testFailed     = false;
                failureMessage = null;
            }
        }

        public synchronized void fail(String message)
        {
            replyReceived = true;
            testFailed = true;
            failureMessage = message;
            this.notifyAll();
            throw new AssertionFailedError();
        }

        public void wrong_reply( String name )
        {
            fail( "wrong reply method: " + name );
        }

        public void wrong_exception( String methodName,
                                     ExceptionHolder excep_holder )
        {
            try
            {
                excep_holder.raise_exception();
            }
            catch( Exception e )
            {
                fail( "unexpected exception: "
                      + methodName + ", " + e );
            }
        }

        public Exception getException( ExceptionHolder excep_holder )
        {
            try
            {
                excep_holder.raise_exception();
                return null;
            }
            catch( Exception e )
            {
                return e;
            }
        }

        public synchronized void pass()
        {
            replyReceived = true;
            testFailed = false;
            failureMessage = null;
            this.notifyAll();
        }

        // The assert methods below are lifted from org.junit.Assert.
        // The only change is to make them non-static here.

        /**
         * Asserts that two bytes are equal.
        */
        public void assertEquals(byte expected, byte actual)
        {
            assertEquals(null, expected, actual);
        }

        /**
         * Asserts that two chars are equal.
         */
        public void assertEquals(char expected, char actual)
        {
            assertEquals(null, expected, actual);
        }

        /**
         * Asserts that two doubles are equal concerning a delta. If the expected
         * value is infinity then the delta value is ignored.
         */
        public void assertEquals(
            double expected,
            double actual,
            double delta)
        {
            assertEquals(null, expected, actual, delta);
        }

        /**
         * Asserts that two floats are equal concerning a delta. If the expected
         * value is infinity then the delta value is ignored.
         */
        public void assertEquals(float expected, float actual, float delta)
        {
            assertEquals(null, expected, actual, delta);
        }

        /**
         * Asserts that two ints are equal.
         */
        public void assertEquals(int expected, int actual)
        {
            assertEquals(null, expected, actual);
        }

        /**
         * Asserts that two longs are equal.
         */
        public void assertEquals(long expected, long actual)
        {
            assertEquals(null, expected, actual);
        }

        /**
         * Asserts that two objects are equal. If they are not
         * an AssertionFailedError is thrown.
         */
        public void assertEquals(Object expected, Object actual)
        {
            assertEquals(null, expected, actual);
        }

        /**
         * Asserts that two bytes are equal.
         */
        public void assertEquals(String message, byte expected, byte actual)
        {
            assertEquals(message, new Byte(expected), new Byte(actual));
        }

        /**
         * Asserts that two chars are equal.
         */
        public void assertEquals(String message, char expected, char actual)
        {
            assertEquals(
                message,
                new Character(expected),
                new Character(actual));
        }

        /**
         * Asserts that two doubles are equal concerning a delta. If the expected
         * value is infinity then the delta value is ignored.
         */
        public void assertEquals(
            String message,
            double expected,
            double actual,
            double delta)
        {
            // handle infinity specially since subtracting to infinite values gives NaN and the
            // the following test fails
            if (Double.isInfinite(expected))
            {
                if (!(expected == actual))
                    failNotEquals(
                        message,
                        new Double(expected),
                        new Double(actual));
            }
            else if (
                !(Math.abs(expected - actual) <= delta))
                // Because comparison with NaN always returns false
                failNotEquals(
                    message,
                    new Double(expected),
                    new Double(actual));
        }

        /**
         * Asserts that two floats are equal concerning a delta. If the expected
         * value is infinity then the delta value is ignored.
         */
        public void assertEquals(
            String message,
            float expected,
            float actual,
            float delta)
        {
            // handle infinity specially since subtracting to infinite values gives NaN and the
            // the following test fails
            if (Float.isInfinite(expected))
            {
                if (!(expected == actual))
                    failNotEquals(
                        message,
                        new Float(expected),
                        new Float(actual));
            }
            else if (!(Math.abs(expected - actual) <= delta))
                failNotEquals(message, new Float(expected), new Float(actual));
        }

        /**
         * Asserts that two ints are equal.
         */
        public void assertEquals(String message, int expected, int actual)
        {
            assertEquals(message, new Integer(expected), new Integer(actual));
        }

        /**
         * Asserts that two longs are equal.
         */
        public void assertEquals(String message, long expected, long actual)
        {
            assertEquals(message, new Long(expected), new Long(actual));
        }

        /**
         * Asserts that two objects are equal. If they are not
         * an AssertionFailedError is thrown.
         */
        public void assertEquals(
            String message,
            Object expected,
            Object actual)
        {
            if (expected == null && actual == null)
                return;
            if (expected != null && expected.equals(actual))
                return;
            failNotEquals(message, expected, actual);
        }

        /**
         * Asserts that two shorts are equal.
         */
        public void assertEquals(String message, short expected, short actual)
        {
            assertEquals(message, new Short(expected), new Short(actual));
        }

        /**
         * Asserts that two booleans are equal.
         */
        public void assertEquals(
            String message,
            boolean expected,
            boolean actual)
        {
            assertEquals(message, new Boolean(expected), new Boolean(actual));
        }

        /**
         * Asserts that two shorts are equal.
         */
        public void assertEquals(short expected, short actual)
        {
            assertEquals(null, expected, actual);
        }

        /**
         * Asserts that two booleans are equal.
         */
        public void assertEquals(boolean expected, boolean actual)
        {
            assertEquals(null, expected, actual);
        }

        /**
         * Asserts that an object isn't null.
         */
        public void assertNotNull(Object object)
        {
            assertNotNull(null, object);
        }

        /**
         * Asserts that an object isn't null.
         */
        public void assertNotNull(String message, Object object)
        {
            assertTrue(message, object != null);
        }

        /**
         * Asserts that an object is null.
         */
        public void assertNull(Object object)
        {
            assertNull(null, object);
        }

        /**
         * Asserts that an object is null.
         */
        public void assertNull(String message, Object object)
        {
            assertTrue(message, object == null);
        }

        /**
         * Asserts that two objects refer to the same object. If they are not
         * the same an AssertionFailedError is thrown.
         */
        public void assertSame(Object expected, Object actual)
        {
            assertSame(null, expected, actual);
        }

        /**
         * Asserts that two objects refer to the same object. If they are not
         * an AssertionFailedError is thrown.
         */
        public void assertSame(String message, Object expected, Object actual)
        {
            if (expected == actual)
                return;
            failNotSame(message, expected, actual);
        }

        /**
         * Asserts that a condition is true. If it isn't it throws
         * an AssertionFailedError with the given message.
         */
        public void assertTrue(String message, boolean condition)
        {
            if (!condition)
                fail(message);
        }

        /**
         * Asserts that a condition is true. If it isn't it throws
         * an AssertionFailedError.
         */
        public void assertTrue(boolean condition)
        {
            assertTrue(null, condition);
        }

        private void failNotEquals( String message,
                                    Object expected,
                                    Object actual )
        {
            String formatted = "";
            if (message != null)
                formatted = message + " ";
            fail( formatted + "expected:<" + expected
                            + "> but was:<" + actual + ">");
        }

        private void failNotSame( String message,
                                  Object expected,
                                  Object actual )
        {
            String formatted = "";
            if (message != null)
                formatted = message + " ";
            fail(formatted + "expected same");
        }

    }

}
