package org.mockejb.test;

import java.rmi.RemoteException;
import java.security.Principal;

import javax.rmi.PortableRemoteObject;

import javax.naming.*;
import javax.ejb.*;

import org.easymock.MockControl;
import org.mockejb.*;
import org.mockejb.interceptor.*;
import org.mockejb.jndi.*;



/**
 * Main MockEjb test case. Demonstrates the basic scenarios of MockEjb usage.
 * This test runs in two modes. When Cactus mode is turned on (using 
 * "mockejb.cactus.mode" system property), it will run as the Cactus test (on the server).
 * In this case, the test class will use EJBs deployed by the app server.
 * Otherwise it will run outside of the app server under MockContainer. 
 * Cactus mode is supported by OptionalCactusTestCase superclass.
 *
 * @author Alexander Ananiev
 */
public class FundamentalsTest extends OptionalCactusTestCase implements Interceptor  {
    
        
    // State of this test case. These variables are initialized by setUp method
    private SampleService sampleService;
    private SampleServiceHome sampleServiceHome;
    private Context context;

    // Aspect system used by this test
    private AspectSystem aspectSystem;

    private MockContainer mockContainer;
    
    /**
     * Flag which is set by our custom interceptor if the method that
     * it checks was called. See testCustomInterceptor. 
     */
    private boolean wasInvoked = false;

        
    public FundamentalsTest(String name) {
        super(name);
    }
        
    /**
     * Deploys and creates EJBs needed for our tests.
     * Note that JUnit runs this method for every test method, but since 
     * MockEjb deployment is purely an in-memory operation, 
     * it does not have any performance penalty.
     * During deployment, MockEjb simply re-binds EJBs to the same keys in the JNDI tree.
     */    
    public void setUp() throws Exception {
        
        /*
         * since some of the tests dynamically add interceptors, 
         * we need to initialize our AspectSystem
         * You may want to use "interceptor.aspect.system.thread" system 
         * property in which case AspectSystem is create per thread so the 
         * tests can run concurrently. 
         */
        aspectSystem =  AspectSystemFactory.getAspectSystem();
        
        /* When the test runs on the server (in cactus mode) we want to be able 
         * to look up EJBs and resources already deployed in the container. 
         * This allows us to run the mix of "true" and  mock EJBs in the same test.
         */
        if ( isRunningOnServer() ) {

            /* MockContext will look up objects in the tree of the
             * app server if its environment is set as the delegate.
             * Since we're already inside the app server, we assume that we can use 
             * its initial context to delegate to. 
             * Note that we must do it before we set MockContextFactory as initial. 
             */
            MockContextFactory.setDelegateContext( new InitialContext() );
        }    

        /* We need to set MockContextFactory as our provider.
         * This method sets the necessary system properties. 
         * We can't simply pass hashtable to the InitialContext 
         * because our tested EJBs have "new InitialContext()" in their code.
         * So we assume that all participants of this test don't use 
         * the constructor of the InitialContext that takes Hashtable.
         */
        MockContextFactory.setAsInitial();
        
        // create the initial context that will be used for binding EJBs
        context = new InitialContext( );
        
        /* Create an instance of the MockContainer.
         * MockContainer also cleans all currently added aspects/interceptors and 
         * adds system interceptors that it  needs, such as ExceptionHandler
         * to make sure that we always start from a known state.
         */ 
        mockContainer = new MockContainer( context );

        // if the test runs outside of the container (app server)
        if (! isRunningOnServer()) {

            /* Deploy EJBs to the MockContainer if we run outside of the app server
             * In cactus mode all but one EJB are deployed by the app server, so we don't need to
             * do it.
             */    
            

            /* Create deployment descriptor of our sample bean.
             * MockEjb does not support XML descriptors.
             */
            SessionBeanDescriptor sampleServiceDescriptor = 
                new SessionBeanDescriptor( SampleService.JNDI_NAME, 
                SampleServiceHome.class, SampleService.class, new SampleServiceBean() );
            // Deploy operation simply creates Home and binds it to JNDI
            mockContainer.deploy( sampleServiceDescriptor );

            // StatelessSampleBean calls SampleHelperBean, so we need to deploy it too
    
            /* MockEjb does not currently support true bean-scoped JNDI context, so 
             * every JNDI name must be unique within the test.
             * Therefore we use JNDI name required by SampleBean (configured via ejb-ref in the real EJB) 
             * to access HelperBean
             */ 
            SessionBeanDescriptor helperBeanDescriptor = 
                new SessionBeanDescriptor( SampleServiceBean.HELPER_BEAN_JNDI_NAME, 
                SampleHelperHome.class, SampleHelper.class, new SampleHelperBean() );
            mockContainer.deploy( helperBeanDescriptor );
    
        }

        /*
         * Deploy mock implementation of the ExternalService session bean.
         * ExternalService is not part of the unit we're testing (SampleBean), 
         * so we don't want to rely on its implementation (which might depend on
         * other session beans or other resources). 
         * We use EasyMock framework to create an implementation that 
         * simply returns mock test data.
         * If you run this class inside the container and real ExternalService session 
         * bean is deployed, our mock 
         * implementation will override it (just for the duration of the test).
         */
        // use easymock to program mock bean. Note that you don't need to worry about callback
        // methods, MockEJB "understands" that the class is a mock class and won't try to 
        // call callback methods on it. 

        MockControl externalServiceControl = MockControl.createControl(ExternalService.class); 
        ExternalService externalServiceBean = (ExternalService) externalServiceControl.getMock(); 
        externalServiceBean.sampleMethod();
        externalServiceControl.setReturnValue("sample string");
        externalServiceControl.replay();

        mockContainer.deploy( new SessionBeanDescriptor( ExternalService.JNDI_NAME, 
                 ExternalServiceHome.class, ExternalService.class, externalServiceBean ) );
        // All EJBs are now deployed
        
        // To get the Sample bean we use the standard J2EE routine
       
        // Lookup the home
        Object sampleHomeObj = context.lookup( SampleService.JNDI_NAME );
        
        // PortableRemoteObject does not do anything in our case but we can still call it
        sampleServiceHome = (SampleServiceHome) PortableRemoteObject.narrow(sampleHomeObj, 
            SampleServiceHome.class );
        
        // create the bean
        sampleService = sampleServiceHome.create();

    }

    
    /**
     * Performs the necessary cleanup by restoring the system properties that
     * were modified by MockContextFactory.setAsInitial().
     * This is needed in case if the test runs inside the container, so it would
     * not affect the tests that run after it.  
     */
    public void tearDown() {
        
        // Inside the container this method does not do anything
        MockContextFactory.revertSetAsInitial();
    }
   
    /**
     * Tests standard Home and EJBMetaData methods.
     */    
    public void testHome() throws Exception {
    
        // Test the support of EJB metadata        
        EJBMetaData ejbMetaData = sampleServiceHome.getEJBMetaData();

        assertEquals( ejbMetaData.getHomeInterfaceClass(), 
            SampleServiceHome.class );
   
        // note how you can use simple "equals" with EJBs and their homes
        assertEquals( sampleServiceHome, ejbMetaData.getEJBHome()  );
                
        // toString also supported
        System.out.println(sampleServiceHome);
        
        // Test standard EJB methods            
        assertEquals( sampleService.getEJBHome(), sampleServiceHome);
        assertTrue( sampleService.isIdentical( sampleService ));
        
        /* make sure that we can serialize home and the bean
         * in case if we run outside of the app server
         */
        if ( !isRunningOnServer() ) {
            java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
            java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(baos);
            oos.writeObject(sampleServiceHome);
            oos.writeObject(sampleService);
            oos.close();        
        }
    
    }
   
    /**
     * Tests simple EJB invocations
     */
    public void testSimpleCalls() throws Exception {
        // Call a simple business method
        String s = sampleService.echoString( "test");
        assertEquals( s, "test" );                     
        
        // now call the method that invokes another bean
        assertNotNull( sampleService.invokeOtherBean() );
        
        // now make a call to the external service
        sampleService.invokeExternalService();
        
        /* We can even examine the internal state of the bean if there is a need.
         * Any EJB created by MockEJB can be casted to the special interface that 
         * provides access to the bean and its context
         */ 
        SampleServiceBean sampleServiceBean =(SampleServiceBean)  ((EjbBeanAccess)sampleService).getBean();
        // we have no  state to check for this test. Just making sure that it works
        assertNotNull(sampleServiceBean);
    } 
   
    
    /**
     * Test MockEJB exception handling. 
     * According to the EJB spec, system exception must be wrapped in 
     * RemoteException.
     * The exception from the local interface must be wrapped in EJBException.
     * MockEjb treats all runtime and transaction-related exceptions as system exceptions.
     */ 
    public void testExceptionHandling() throws Exception { 

        try{
            sampleService.throwSystemException();
        }
        catch ( RemoteException remoteEx ){
            assertTrue( remoteEx.detail instanceof EJBException );
            assertTrue( ((EJBException)remoteEx.detail).getCausedByException() 
                instanceof RuntimeException );           
        }            
        
        // Example of the application exception.
        // Container should pass this exception through without changes.
        try{
            sampleService.throwAppException();
            fail("We did not get the application exception ");
        }
        catch( Exception ex ){
        }
    }
    
    /**
     * Demonstrates the use of custom interceptors. 
     * This test class implements Interceptor interface. It will verify
     * that not-null parameter is passed to HelperBean.
     * The same thing can be done using InvocationRecorder 
     * (see testInvocationRecorder).
     * We can only run this test if it is being executed outside of the 
     * container. 
     */
    public void testCustomInterceptor() throws Exception {
        
        // This won't work in the container since it does not support interceptors
        if ( isRunningOnServer( ))
            return;
        
        // Set our custom interceptor so it would handle all calls to SampleHelper interface (w/o subclasses)
        aspectSystem.add( new ClassPointcut( SampleHelper.class, false), this);        
        
        // flag indicating that the EJB under test was called
        wasInvoked = false;
         
        // we can now invoke our test method
        // In case of problems, "asserts" in the invoker will fail the test.
        // This method calls HelperBean method
        sampleService.invokeOtherBean();
        // we need to make sure that the method was invoked
        assertTrue( wasInvoked );
        
    }


    /**
     * InvocationRecorder interceptor provides an easy way to inspect the
     * data about the calls.
     * If it added to the interceptor list, it will record the information about
     * all calls to the bean.
     * We'll use it to check that SampleBean called HelperBean and passed non-null
     * parameter.
     * We can only run this test if it is being executed outside of the 
     * container. 
     */
    public void testInvocationRecorder() throws Exception {

        // This won't work in the container since it does not support interceptors
        if ( isRunningOnServer( ))
            return;

        InvocationRecorder recorder = new InvocationRecorder();
        
        // Set the recorder so it would record all calls to SampleHelper interface (w/o subclasses)
        aspectSystem.add( new ClassPointcut( SampleHelper.class, false), recorder);        
        
        /*
         * We can also capture the calls to the create method of the SampleHelperHome
         * We don't really need to do it here, it just demonstrates that you can intercept 
         * "create" methods. 
         */  
        aspectSystem.add( new MethodPatternPointcut( "SampleHelperHome\\.create()" ), recorder);
        
        /*
         * Or how about intercepting JNDI lookups? 
         */  
        aspectSystem.add( new MethodPatternPointcut( "Context\\.lookup" ), recorder);
        

        // recorder is now active, it will preserve the info about all calls to SampleHelper

        // we can now invoke our test method
        // This method calls HelperBean method
        sampleService.invokeOtherBean();
        
        
        // Make sure that the method of our interest was invoked
        
        // Tests that the JNDI lookup was called
        assertNotNull( recorder.findByProxyMethod( "Context\\.lookup") );
        
        // Test that the lifecycle methods were invoked by the container (triggered by "create")
        assertNotNull( recorder.findByProxyMethod( "SampleHelperHome\\.create()") );
        
        
        // Test that the dummy method was invoked
        InvocationContext dummyMethodInvocation = recorder.findByTargetMethod( "dummyMethod");
        assertNotNull(dummyMethodInvocation);
        // make sure that the parameter was not null
        assertNotNull( dummyMethodInvocation.getParamVals()[0]);
        
        // since the aspect system is re-initialized in setUp we don't need to 
        // clean the recorder we just added        
    }
    /**
     * This method demonstrates how to test beans that makes the use of
     * use of the security-related methods of EJBContext (getCallerPrincipal, isCallerInRole).
     */
    public void testSecurity() throws Exception {

        /* Create the mock user and "login". Login method
         * simply puts the user on the thread so EJBContext implementation
         * can get to it. 
         * This will create user without any roles.
         */ 
        mockContainer.login( new MockUser( "testuser"));
        Principal principal = sampleService.getPrincipal();
        assertEquals("testuser", principal.getName());
        
        // Now let's add roles
        mockContainer.login( new MockUser( "testuser", new String[] {"role1","role2"}));
        // "hasRole" simply returns the result of "isCallerInRole"
        assertTrue( sampleService.hasRole("role1") );
        assertTrue( sampleService.hasRole("role2") );
        assertFalse( sampleService.hasRole("fake_role") );
    
        // If you don't login, MockContainer logs in as anonymous user, 
        // which you can do as well:
        mockContainer.login( MockUser.ANONYMOUS_USER );
    }

    
    /**
     * Implementation of the Interceptor interface that can be used 
     * for testing various preconditions/postconditions of the EJB call.
     * Here we use it for very simple check to make sure that non-null parameter
     * is passed to the HelperBean.
     */
    public void intercept( InvocationContext invocationContext ) throws Exception {
        
        // Make sure that we run the test for the right method
        if ( SampleHelperBean.class.isInstance( invocationContext.getTargetObject() ) && 
                invocationContext.getTargetMethod().getName().equals("dummyMethod")) { 
            System.out.println("Running test invoker for "+invocationContext.getTargetMethod() );
            // Test that SampleBean is passing parameter to the HelperBean
            assertNotNull( invocationContext.getParamVals()[0] );
            wasInvoked = true;            
        }
        
        // We're done, move to the next interceptor from the list
        invocationContext.proceed( );
    }
    
    /**
     * Example of a mock session bean implementing ExternalService interface.
     * Note how MockEJB allows to specify a bean implementation without having to
     * provide mandatory callback methods. 
     * This class is not used. It is here only for demo purposes. 
     * We use Easymock to create mock implementation
     * classes. 
     */
    public static class ExternalServiceMockBean  { 
        
        public String sampleMethod(){
            return "sample result";
        }
        
    }        
        
}