package org.jacorb.test.orb.policies;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import junit.framework.AssertionFailedError;
import org.jacorb.test.AMI_TimingServerHandler;
import org.jacorb.test.AMI_TimingServerHandlerOperations;
import org.jacorb.test.AMI_TimingServerHandlerPOATie;
import org.jacorb.test.EmptyException;
import org.jacorb.test.TimingServer;
import org.jacorb.test.TimingServerHelper;
import org.jacorb.test._TimingServerStub;
import org.jacorb.test.harness.CallbackTestCase;
import org.jacorb.test.harness.ClientServerSetup;
import org.jacorb.util.Time;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.omg.CORBA.Policy;
import org.omg.CORBA.PolicyError;
import org.omg.CORBA.SetOverrideType;
import org.omg.Messaging.ExceptionHolder;
import org.omg.Messaging.RELATIVE_REQ_TIMEOUT_POLICY_TYPE;
import org.omg.Messaging.RELATIVE_RT_TIMEOUT_POLICY_TYPE;
import org.omg.Messaging.REPLY_END_TIME_POLICY_TYPE;
import org.omg.Messaging.REPLY_START_TIME_POLICY_TYPE;
import org.omg.Messaging.REQUEST_END_TIME_POLICY_TYPE;
import org.omg.Messaging.REQUEST_START_TIME_POLICY_TYPE;
import org.omg.TimeBase.UtcT;
import org.omg.TimeBase.UtcTHelper;

/**
 * @author Andre Spiegel spiegel@gnu.org
 */
public class TimingTest extends CallbackTestCase
{
    private TimingServer server = null;

    @Before
    public void setUp() throws Exception
    {
        server = TimingServerHelper.narrow (setup.getServerObject());
    }

    private class ReplyHandler
        extends CallbackTestCase.ReplyHandler
        implements AMI_TimingServerHandlerOperations
    {

        public void ex_op_excep(ExceptionHolder excep_holder)
        {
            wrong_exception ("ex_op_excep", excep_holder);
        }

        public void ex_op(char ami_return_val)
        {
            wrong_reply ("ex_op");
        }

        public void operation_excep(ExceptionHolder excep_holder)
        {
            wrong_exception ("operation_excep", excep_holder);
        }

        public void operation(int ami_return_val)
        {
            wrong_reply ("operation");
        }

        public void server_time_excep(ExceptionHolder excep_holder)
        {
            wrong_exception ("server_time_excep", excep_holder);
        }

        public void server_time(long ami_return_val)
        {
            wrong_reply ("server_time");
        }

    }

    private AMI_TimingServerHandler ref ( ReplyHandler handler )
    {
        AMI_TimingServerHandlerPOATie tie =
            new AMI_TimingServerHandlerPOATie( handler )
            {
                @Override
                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() );
    }

    @BeforeClass
    public static void beforeClassSetUp() throws Exception
    {

        setup = new ClientServerSetup
        (

                 "org.jacorb.test.orb.policies.TimingServerImpl");

        // some tests are disabled below (no test prefix)
        // because it is impossible
        // to make them succeed on a fast machine where the Java
        // clock has only millisecond resolution

    }

    /**
     * Do a few synchronous invocations as a sanity check
     * and to get all the necessary classes loaded.
     */
    @Test
    public void test_sync_no_timing() throws Exception
    {
        int result = server.operation (1, 0);
        assertEquals (1, result);

        result = server.operation (2, 10);
        assertEquals (2, result);

        result = server.operation (3, 100);
        assertEquals (3, result);

        try
        {
            server.ex_op('e', 50);
            fail ("should have raised EmptyException");
        }
        catch (EmptyException ex)
        {
            // ok
        }

        try
        {
            server.ex_op('$', 50);
            fail ("should have raised DATA_CONVERSION");
        }
        catch (org.omg.CORBA.DATA_CONVERSION ex)
        {
            // ok
        }
    }

    /**
     * Do a few asynchronous invocations as a sanity check
     * and to get all the necessary classes loaded.
     */
    @Test
    public void test_async_no_timing()
    {
        ReplyHandler handler = new ReplyHandler()
        {
            @Override
            public void operation (int ami_return_val)
            {
                this.assertEquals (7, ami_return_val);
                pass();
            }
        };
        ((_TimingServerStub)server).sendc_operation (ref (handler), 7, 50);
        handler.wait_for_reply(400);

        handler = new ReplyHandler()
        {
            @Override
            public void ex_op_excep (ExceptionHolder excep_holder)
            {
                this.assertEquals (EmptyException.class,
                              getException (excep_holder).getClass());
                pass();
            }
        };
        ((_TimingServerStub)server).sendc_ex_op (ref (handler), 'e', 50);
        handler.wait_for_reply(400);

        handler = new ReplyHandler()
        {
            @Override
            public void ex_op_excep (ExceptionHolder excep_holder)
            {
                this.assertEquals (org.omg.CORBA.DATA_CONVERSION.class,
                              getException (excep_holder).getClass());
                pass();
            }
        };
        ((_TimingServerStub)server).sendc_ex_op (ref (handler), '$', 50);
        handler.wait_for_reply(400);
    }

    /**
     * Set all timing policies to values that will be met by the invocation.
     */
    @Test
    public void test_all_policies_sync_ok() throws Exception
    {
        server = clearPolicies (server);
        server = setRequestStartTime (server, System.currentTimeMillis());
        server = setRequestEndTime (server, System.currentTimeMillis() + 2000);
        server = setRelativeRequestTimeout (server, System.currentTimeMillis() + 3000);
        server = setReplyStartTime (server, System.currentTimeMillis());
        server = setReplyEndTime (server, System.currentTimeMillis() + 5000);
        server = setRelativeRoundtripTimeout(server, System.currentTimeMillis() + 6000);

        assertEquals(434, server.operation (434, 500));
    }


    /**
     * Sets a RequestStartTime which will already have expired
     * when the request arrives.
     */
    @Test
    public void test_request_start_time_sync_expired()
    {
        server = clearPolicies (server);
        long start = System.currentTimeMillis();
        server = setRequestStartTime (server, start);
        server.server_time (10);

        long delta = System.currentTimeMillis() - start;
        if (delta > 200)
        {
            fail ("reply too late (" + delta + "ms)");
        }
    }

    /**
     * Sets a RequestStartTime which will not have been reached
     * when the request arrives.
     */
    @Test
    public void test_request_start_time_sync_wait()
    {
        server = clearPolicies (server);
        long start = System.currentTimeMillis();
        server = setRequestStartTime (server, start + 200);
        long time = server.server_time (100);

        long delta = time - start;
        if (delta < 200)
        {
            fail ("request started too early (" + delta + "ms)");
        }
        else if (delta > 250)
        {
            fail ("request started too late (" + delta + "ms)");
        }
    }

    /**
     * Sets a RequestEndTime which will
     * be met by the invocation.
     */
    @Test
    public void test_request_end_time_sync_ok() throws Exception
    {
        server = clearPolicies (server);
        server = setRequestEndTime (server, System.currentTimeMillis() + 400);
        server.operation (434, 500);
    }

    /**
     * Sets a RequestEndTime which will have expired prior
     * to the invocation.
     */
    @Test
    public void test_request_end_time_sync_pre_expired()
    {
        server = clearPolicies (server);
        server = setRequestEndTime (server, System.currentTimeMillis() - 200);
        try
        {
            server.operation (121, 50);
            fail ("should have been a TIMEOUT");
        }
        catch (org.omg.CORBA.TIMEOUT t)
        {
            // ok
        }
    }

    /**
     * Sets a RequestEndTime which will have expired prior to the invocation.
     */
    @Test
    public void test_request_end_time_async_pre_expired()
    {
        ReplyHandler handler = new ReplyHandler();

        server = clearPolicies (server);
        server = setRequestEndTime (server, System.currentTimeMillis() - 5);
        try
        {
            ((_TimingServerStub)server).sendc_operation (ref (handler), 765, 50);
            fail ("should have been a TIMEOUT");
        }
        catch (org.omg.CORBA.TIMEOUT e)
        {
            // ok
        }
    }

    /**
     * Sets a RequestEndTime which will expire during the invocation.
     */
    @Test
    public void test_request_end_time_sync_expired() throws Exception
    {
        server = clearPolicies (server);
        server = setRequestEndTime (server, System.currentTimeMillis() - 200);
        try
        {
            server.operation (121, 50);
            fail ("should have been a TIMEOUT");
        }
        catch (org.omg.CORBA.TIMEOUT t)
        {
            // ok
        }
    }


    /**
     * Sets a RelativeRequestTimeout which will
     * be met by the invocation.
     */
    @Test
    public void test_request_timeout_sync_ok()
    {
        server = clearPolicies (server);
        server = setRelativeRequestTimeout (server, 200);
        try
        {
            server.operation (434, 300);
        }
        catch (org.omg.CORBA.TIMEOUT t)
        {
            fail ("should not have been a TIMEOUT");
        }
    }

    /**
     * Sets a RelativeRequestTimeout which will expire during the invocation.
     */
    @Ignore ("### Ignore pre-junit4 disabled test")
    @Test
    public void test_request_timeout_sync_expired()
    {
        server = clearPolicies (server);
        server = setRelativeRequestTimeout (server, 1);

        try
        {
            server.operation (121, 50);
            fail ("should have been a TIMEOUT");
        }
        catch (org.omg.CORBA.TIMEOUT t)
        {
            // ok
        }
    }

    /**
     * Sets a RelativeRequestTimeout which will
     * expire during the invocation.
     */
    @Ignore ("### Ignore pre-junit4 disabled test")
    @Test
    public void test_request_timeout_async_expired()
    {
        ReplyHandler handler = new ReplyHandler()
        {
            @Override
            public void operation (int ami_return_val)
            {
                this.fail ("should have raised TIMEOUT");
            }

            @Override
            public void operation_excep (ExceptionHolder excep_holder)
            {
                this.assertEquals (org.omg.CORBA.TIMEOUT.class,
                              getException (excep_holder).getClass());
                pass();
            }
        };

        server = clearPolicies (server);
        server = setRelativeRequestTimeout (server, -100);
        ((_TimingServerStub)server).sendc_operation (ref (handler), 767, 200);
        handler.wait_for_reply (400);
    }

    /**
     * Sets a ReplyStartTime which will already have expired
     * when the reply arrives.
     */
    @Test
    public void test_reply_start_time_sync_expired()
    {
        server = clearPolicies (server);
        long start = System.currentTimeMillis();
        server = setReplyStartTime (server, start);
        server.operation (18, 10);

        long delta = System.currentTimeMillis() - start;
        if (delta > 200)
        {
            fail ("reply too late (" + delta + "ms)");
        }
    }

    /**
     * Sets a ReplyStartTime which will not have been reached
     * when the reply arrives.
     */
    @Test
    public void test_reply_start_time_sync_wait()
    {
        server = clearPolicies (server);
        long start = System.currentTimeMillis();
        server = setReplyStartTime (server, start + 2000);
        int result = server.operation (18, 1000);
        assertEquals (18, result);

        long delta = System.currentTimeMillis() - start;
        if (delta < 2000)
        {
            fail ("reply too early (" + delta + "ms)");
        }
        else if (delta > 2500)
        {
            fail ("reply too late (" + delta + "ms)");
        }
    }

    /**
     * Sets a ReplyStartTime which will already have expired
     * when the reply arrives.
     */
    @Test
    public void test_reply_start_time_async_expired()
    {
        ReplyHandler handler = new ReplyHandler()
        {
            @Override
            public void operation (int ami_return_val)
            {
                this.assertEquals (19, ami_return_val);
                pass();
            }
        };

        server = clearPolicies (server);
        long start = System.currentTimeMillis();
        server = setReplyStartTime (server, start);
        ((_TimingServerStub)server).sendc_operation (ref (handler), 19, 500);
        handler.wait_for_reply (1000);
        long delta = System.currentTimeMillis() - start;
        if (delta > 2000)
        {
            fail ("reply too late (" + delta + "ms)");
        }
    }

    /**
     * Sets a ReplyStartTime which will not have been reached
     * when the reply arrives.
     */
    @Test
    public void test_reply_start_time_async_wait()
    {
        ReplyHandler handler = new ReplyHandler()
        {
            @Override
            public void operation (int ami_return_val)
            {
                this.assertEquals (19, ami_return_val);
                pass();
            }
        };

        server = clearPolicies (server);
        long start = System.currentTimeMillis();
        server = setReplyStartTime (server, start + 200);
        ((_TimingServerStub)server).sendc_operation (ref (handler), 19, 100);
        handler.wait_for_reply (250);

        long delta = System.currentTimeMillis() - start;
        if (delta < 200)
        {
            fail ("reply too early (" + delta + "ms)");
        }
    }

    /**
     * Sets a ReplyEndTime which will
     * be met by the invocation.
     */
    @Test
    public void test_reply_end_time_async_ok()
    {
        ReplyHandler handler = new ReplyHandler()
        {
            @Override
            public void operation (int ami_return_val)
            {
                this.assertEquals (765, ami_return_val);
                pass();
            }
        };

        server = clearPolicies (server);
        server = setReplyEndTime (server, System.currentTimeMillis() + 500);
        ((_TimingServerStub)server).sendc_operation (ref (handler), 765, 50);
        handler.wait_for_reply (450);
    }


    /**
     * Sets a ReplyEndTime which will
     * expire during the invocation.
     */
    @Test
    public void test_reply_end_time_async_expired()
    {
        ReplyHandler handler = new ReplyHandler()
        {
            @Override
            public void operation (int ami_return_val)
            {
                this.fail ("should have raised TIMEOUT");
            }

            @Override
            public void operation_excep (ExceptionHolder excep_holder)
            {
                this.assertEquals (org.omg.CORBA.TIMEOUT.class,
                              getException (excep_holder).getClass());
                pass();
            }
        };

        server = clearPolicies (server);
        server = setReplyEndTime (server, System.currentTimeMillis() + 1000);
        ((_TimingServerStub)server).sendc_operation (ref (handler), 767, 2000);
        handler.wait_for_reply (4000);
    }

    /**
     * Sets a ReplyEndTime which will
     * be met by the invocation.
     */
    @Test
    public void test_reply_end_time_sync_ok()
    {
        server = clearPolicies (server);
        server = setReplyEndTime (server, System.currentTimeMillis() + 2000);
        server.operation (434, 500);
    }

    /**
     * Sets a ReplyEndTime which has expired prior to invocation.
     */
    @Test
    public void test_reply_end_time_sync_pre_expired()
    {
        server = clearPolicies (server);
        server = setReplyEndTime (server, System.currentTimeMillis() - 100);
        try
        {
            server.operation (44, 100);
            fail ("should have raised TIMEOUT");
        }
        catch (org.omg.CORBA.TIMEOUT t)
        {
            // ok
        }
    }

    /**
     * Sets a ReplyEndTime which will
     * expire during invocation.
     */
    @Test
    public void test_reply_end_time_sync_expired()
    {
        server = clearPolicies (server);
        server = setReplyEndTime (server, System.currentTimeMillis() + 200);
        try
        {
            server.operation (343, 300);
            fail ("should have raised TIMEOUT");
        }
        catch (org.omg.CORBA.TIMEOUT t)
        {
            // ok
        }
    }


    /**
     * Sets a RelativeRoundtripTimeout which will
     * be met by the invocation.
     */
    @Test
    public void test_relative_roundtrip_sync_ok()
    {
        server = clearPolicies (server);
        server = setRelativeRoundtripTimeout (server, 200);
        server.operation (434, 50);
    }

    /**
     * Sets a RelativeRoundtripTimeout which will
     * expire during invocation.
     */
    @Test
    public void test_relative_roundtrip_sync_expired()
    {
        server = clearPolicies (server);
        server = setRelativeRoundtripTimeout (server, 200);
        try
        {
            server.operation (343, 300);
            fail ("should have raised TIMEOUT");
        }
        catch (org.omg.CORBA.TIMEOUT t)
        {
            // ok
        }
    }

    /**
     * Sets a RelativeRoundtripTimeout which will
     * be met by the invocation.
     */
    @Test
    public void test_relative_roundtrip_async_ok()
    {
        ReplyHandler handler = new ReplyHandler()
        {
            @Override
            public void operation (int ami_return_val)
            {
                this.assertEquals (765, ami_return_val);
                pass();
            }
        };

        server = clearPolicies (server);
        server = setRelativeRoundtripTimeout (server, 200);
        ((_TimingServerStub)server).sendc_operation (ref (handler), 765, 50);
        handler.wait_for_reply (150);
    }

    /**
     * Sets a RelativeRoundtripTimeout which will
     * expire during the invocation.
     */
    @Test
    public void test_relative_roundtrip_async_expired()
    {
        ReplyHandler handler = new ReplyHandler()
        {
            @Override
            public void operation (int ami_return_val)
            {
                this.fail ("should have raised TIMEOUT");
            }

            @Override
            public void operation_excep (ExceptionHolder excep_holder)
            {
                this.assertEquals (org.omg.CORBA.TIMEOUT.class,
                              getException (excep_holder).getClass());
                pass();
            }
        };

        server = clearPolicies (server);
        server = setRelativeRoundtripTimeout (server, 50);
        ((_TimingServerStub)server).sendc_operation (ref (handler), 767, 100);
        handler.wait_for_reply (400);
    }

    /**
     * Test multiple combined policies.
     * Sets a Request- and ReplyStartTime which will not have been
     * reached. Server needs to wait for both policies.
     */
    @Test
    public void test_request_reply_start_time_sync_wait()
    {
        server = clearPolicies (server);
        long start = System.currentTimeMillis();
        server = setRequestStartTime(server, start + 2000);
        server = setReplyStartTime(server, start + 6000);
        long serverStart = server.server_time(2000);
        long rtTime = System.currentTimeMillis() - start;

        // check server starts immediately
        assertTrue(serverStart >= start + 2000);
        assertTrue(serverStart <= start  + 2000 + 500); // 500 ms latency

        // check roundtrip time takes 1000 ms
        assertTrue(rtTime >= 6000);
        assertTrue(rtTime <= 6000 + 500); // 500 ms latency
    }


    // convenience methods for policy manipulation

    // These methods create policies in the really cumbersome way
    // via the ORB, so that the mechanism gets tested.  Each of the
    // policy types in org.jacorb.orb.policies also has a convenience
    // constructor that makes it much easier.

    private TimingServer clearPolicies (TimingServer server)
    {
        org.omg.CORBA.Object r = server._set_policy_override (new Policy[]{}, SetOverrideType.SET_OVERRIDE);
        server._release();
        return TimingServerHelper.narrow (r);
    }

    private TimingServer setRequestStartTime (TimingServer server, long unixTime)
    {
        UtcT corbaTime = Time.corbaTime (unixTime);

        org.omg.CORBA.ORB orb = setup.getClientOrb();
        org.omg.CORBA.Any any = orb.create_any();
        UtcTHelper.insert (any, corbaTime);
        try
        {
            Policy policy =
                orb.create_policy (REQUEST_START_TIME_POLICY_TYPE.value, any);
            org.omg.CORBA.Object r = server._set_policy_override (new Policy[]{ policy },
                                         SetOverrideType.ADD_OVERRIDE);
            server._release();
            return TimingServerHelper.narrow (r);
        }
        catch (PolicyError e)
        {
            throw new RuntimeException ("policy error: " + e);
        }
    }

    private TimingServer setRequestEndTime (TimingServer server, long unixTime)
    {
        UtcT corbaTime = Time.corbaTime (unixTime);

        org.omg.CORBA.ORB orb = setup.getClientOrb();
        org.omg.CORBA.Any any = orb.create_any();
        UtcTHelper.insert (any, corbaTime);
        try
        {
            Policy policy =
                orb.create_policy (REQUEST_END_TIME_POLICY_TYPE.value, any);
            org.omg.CORBA.Object r = server._set_policy_override (new Policy[]{ policy },
                                         SetOverrideType.ADD_OVERRIDE);
            server._release();
            return TimingServerHelper.narrow (r);
        }
        catch (PolicyError e)
        {
            throw new RuntimeException ("policy error: " + e);
        }
    }

    private TimingServer setRelativeRequestTimeout (TimingServer server,
                                              long millis)
    {
        org.omg.CORBA.ORB orb = setup.getClientOrb();
        org.omg.CORBA.Any any = orb.create_any();
        any.insert_ulonglong (millis * 10000);
        try
        {
            Policy policy =
                orb.create_policy (RELATIVE_REQ_TIMEOUT_POLICY_TYPE.value, any);
            org.omg.CORBA.Object r = server._set_policy_override (new Policy[]{ policy },
                                         SetOverrideType.ADD_OVERRIDE);
            server._release();
            return TimingServerHelper.narrow (r);
        }
        catch (PolicyError e)
        {
            throw new RuntimeException ("policy error: " + e);
        }
    }

    private TimingServer setReplyStartTime (TimingServer server, long unixTime)
    {
        UtcT corbaTime = Time.corbaTime (unixTime);

        org.omg.CORBA.ORB orb = setup.getClientOrb();
        org.omg.CORBA.Any any = orb.create_any();
        UtcTHelper.insert (any, corbaTime);
        try
        {
            Policy policy =
                orb.create_policy (REPLY_START_TIME_POLICY_TYPE.value, any);
            org.omg.CORBA.Object r = server._set_policy_override (new Policy[]{ policy },
                                         SetOverrideType.ADD_OVERRIDE);
            server._release();
            return TimingServerHelper.narrow (r);
        }
        catch (PolicyError e)
        {
            throw new RuntimeException ("policy error: " + e);
        }
    }

    private TimingServer setReplyEndTime (TimingServer server, long unixTime)
    {
        UtcT corbaTime = Time.corbaTime (unixTime);

        org.omg.CORBA.ORB orb = setup.getClientOrb();
        org.omg.CORBA.Any any = orb.create_any();
        UtcTHelper.insert (any, corbaTime);
        try
        {
            Policy policy =
                orb.create_policy (REPLY_END_TIME_POLICY_TYPE.value, any);
            org.omg.CORBA.Object r = server._set_policy_override (new Policy[]{ policy },
                                         SetOverrideType.ADD_OVERRIDE);
            server._release();
            return TimingServerHelper.narrow (r);
        }
        catch (PolicyError e)
        {
            throw new RuntimeException ("policy error: " + e);
        }
    }

    private TimingServer setRelativeRoundtripTimeout (TimingServer server,
                                              long millis)
    {
        org.omg.CORBA.ORB orb = setup.getClientOrb();
        org.omg.CORBA.Any any = orb.create_any();
        any.insert_ulonglong (millis * 10000);
        try
        {
            Policy policy =
                orb.create_policy (RELATIVE_RT_TIMEOUT_POLICY_TYPE.value, any);
            org.omg.CORBA.Object r = server._set_policy_override (new Policy[]{ policy },
                                         SetOverrideType.ADD_OVERRIDE);
            server._release();
            return TimingServerHelper.narrow (r);
        }
        catch (PolicyError e)
        {
            throw new RuntimeException ("policy error: " + e);
        }
    }
}
