/*******************************************************************************
 * Copyright (c) 2006 Wind River Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.tests.dsf.concurrent;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor;
import org.eclipse.cdt.dsf.concurrent.ImmediateInDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.Query;
import org.eclipse.cdt.dsf.concurrent.RequestCache;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.tests.dsf.DsfTestPlugin;
import org.eclipse.cdt.tests.dsf.TestDsfExecutor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * Tests that exercise the DataCache object.
 */
public class CacheTests {

    TestDsfExecutor fExecutor;
    TestCache fTestCache;
    DataRequestMonitor<Integer> fRetrieveRm;
    
    class TestCache extends RequestCache<Integer> {
        
        public TestCache() {
            super(new ImmediateInDsfExecutor(fExecutor));
        }

        @Override
        protected void retrieve(DataRequestMonitor<Integer> rm) {
            synchronized(CacheTests.this) {
                fRetrieveRm = rm;
                CacheTests.this.notifyAll();
            }
        }
        
        @Override
        protected void reset() {
            super.reset();
        }
        
        @Override
        public void set(Integer data, IStatus status) {
            super.set(data, status);
        }
        
    }

    class TestQuery extends Query<Integer> {
        @Override
        protected void execute(final DataRequestMonitor<Integer> rm) {
            fTestCache.update(new RequestMonitor(ImmediateExecutor.getInstance(), rm) {
                @Override
                protected void handleSuccess() {
                    rm.setData(fTestCache.getData());
                    rm.done();
                }
            });
        }
    }
    
    /**
     * There's no rule on how quickly the cache has to start data retrieval
     * after it has been requested.  It could do it immediately, or it could
     * wait a dispatch cycle, etc..
     */
    private void waitForRetrieveRm() {
        synchronized(this) {
            while (fRetrieveRm == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    return;
                }
            }
        }
    }
    
    @Before 
    public void startExecutor() throws ExecutionException, InterruptedException {
        fExecutor = new TestDsfExecutor();
        fTestCache = new TestCache();
    }   
    
    @After 
    public void shutdownExecutor() throws ExecutionException, InterruptedException {
        fExecutor.submit(new DsfRunnable() { public void run() {
            fExecutor.shutdown();
        }}).get();
        if (fExecutor.exceptionsCaught()) {
            Throwable[] exceptions = fExecutor.getExceptions();
            throw new ExecutionException(exceptions[0]);
        }
        fRetrieveRm = null;
        fTestCache = null;
        fExecutor = null;
    }

    private void assertCacheValidWithData(Object data) {
        assertTrue(fTestCache.isValid());
        assertEquals(data, fTestCache.getData());
        assertTrue(fTestCache.getStatus().isOK());
    }

    private void assertCacheResetWithoutData() {
        assertFalse(fTestCache.isValid());
        try {
            fTestCache.getData();
            fail("Expected an IllegalStateException");
        } catch (IllegalStateException e) {}
        try {
            fTestCache.getStatus();
            fail("Expected an IllegalStateException");
        } catch (IllegalStateException e) {}
    }

    private void assertCacheValidWithoutData() {
        assertTrue(fTestCache.isValid());
        assertEquals(null, fTestCache.getData());
        assertFalse(fTestCache.getStatus().isOK());
        assertEquals(fTestCache.getStatus().getCode(), ERRCODE_TARGET_RUNNING);
    }

    private void assertCacheWaiting() {
        assertFalse(fTestCache.isValid());
        try {
            fTestCache.getData();
            fail("Expected an IllegalStateException");
        } catch (IllegalStateException e) {}
        try {
            fTestCache.getStatus();
            fail("Expected an IllegalStateException");
        } catch (IllegalStateException e) {}
        assertFalse(fRetrieveRm.isCanceled());
    }

    private void assertCacheInvalidAndWithCanceledRM() {
        assertFalse(fTestCache.isValid());
        try {
            fTestCache.getData();
            fail("Expected an IllegalStateException");
        } catch (IllegalStateException e) {}
        try {
            fTestCache.getStatus();
            fail("Expected an IllegalStateException");
        } catch (IllegalStateException e) {}
        assertTrue(fRetrieveRm.isCanceled());
    }

    @Test 
    public void getWithCompletionInDsfThreadTest() throws InterruptedException, ExecutionException {
        // Request data from cache
        Query<Integer> q = new TestQuery();
        
        // Check initial state
        assertFalse(fTestCache.isValid());
        
        fExecutor.execute(q);
        
        // Wait until the cache requests the data.
        waitForRetrieveRm();
        
        // Check state while waiting for data
        assertFalse(fTestCache.isValid());

        // Complete the cache's retrieve data request.
        fExecutor.submit(new Callable<Object>() { public Object call() {
            fRetrieveRm.setData(1);
            fRetrieveRm.done();

            // Check that the data is available in the cache immediately
            // (in the same dispatch cycle).
            assertEquals(1, (int)fTestCache.getData());
            assertTrue(fTestCache.isValid());
            
            return null;
        }}).get();
        
        assertEquals(1, (int)q.get());
        
        // Re-check final state
        assertCacheValidWithData(1);
    }

    @Test 
    public void getTest() throws InterruptedException, ExecutionException {
        // Check initial state
        assertFalse(fTestCache.isValid());
        
        // Request data from cache
        Query<Integer> q = new TestQuery();
        fExecutor.execute(q);
        
        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();

        // Check state while waiting for data
        assertFalse(fTestCache.isValid());

        // Set the data without using an executor.  
        fRetrieveRm.setData(1);
        fRetrieveRm.done();
        
        assertEquals(1, (int)q.get());

        // Check final state
        assertCacheValidWithData(1);
    }

    @Test 
    public void getTestWithTwoClients() throws InterruptedException, ExecutionException {
        // Check initial state
        assertFalse(fTestCache.isValid());
        
        // Request data from cache
        Query<Integer> q1 = new TestQuery();
        fExecutor.execute(q1);

        // Request data from cache again
        Query<Integer> q2 = new TestQuery(); 
        fExecutor.execute(q2);
        
        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();

        // Check state while waiting for data
        assertFalse(fTestCache.isValid());

        // Set the data without using an executor.  
        fRetrieveRm.setData(1);
        fRetrieveRm.done();
        
        assertEquals(1, (int)q1.get());
        assertEquals(1, (int)q2.get());

        // Check final state
        assertCacheValidWithData(1);
    }

    @Test 
    public void getTestWithManyClients() throws InterruptedException, ExecutionException {
        // Check initial state
        assertFalse(fTestCache.isValid());
        
        // Request data from cache
        List<Query<Integer>> qList = new ArrayList<Query<Integer>>(); 
        for (int i = 0; i < 10; i++) {
            Query<Integer> q = new TestQuery();
            fExecutor.execute(q);
            qList.add(q);
        }
        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();

        // Check state while waiting for data
        assertFalse(fTestCache.isValid());

        // Set the data without using an executor.  
        fRetrieveRm.setData(1);
        fRetrieveRm.done();

        for (Query<Integer> q : qList) {
            assertEquals(1, (int)q.get());            
        }
        
        // Check final state
        assertCacheValidWithData(1);
    }
    
    private static final int ERRCODE_TARGET_RUNNING = 1234;
    private static final Status STATUS_TARGET_RUNNING = new Status(Status.ERROR, DsfTestPlugin.PLUGIN_ID, ERRCODE_TARGET_RUNNING, "Target is running", null);
    
	// DISABLE TESTS
	//
	// We say a cache is "disabled" when its most recent attempt to update from
	// the source failed. Also, a cache may make itself disabled as a reaction
	// to a state change notification from its source (e.g., the target
	// resumed). In either case, the cache is in the valid state but it has no
	// data and the status reflects an error. Keep in mind that the 'valid'
	// state is not a reflection of the quality of the data, but merely whether
	// the cache object's representation of the data is stale or
	// not. A transaction that uses a "disabled" cache object will simply fail;
	// it will not ask the cache to update its data from the source. Only a
	// change in the source's state would cause the cache to put itself back in
	// the invalid state, thus opening the door to another update.

	/**
	 * Test behavior when a cache object is asked to update itself after it has
	 * become "disabled". Since a "disabled" cache is in the valid state, a
	 * request for it to update from the source should be ignored.
	 */
    @Test 
    public void disableBeforeRequestTest() throws InterruptedException, ExecutionException {
        // Disable the cache
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fTestCache.set(null, STATUS_TARGET_RUNNING);
            }
        }).get();
        
        assertCacheValidWithoutData();
        
        // Try to request data from cache
        Query<Integer> q = new TestQuery(); 
        fExecutor.execute(q);
        
        Thread.sleep(100);
        
        // Retrieval should never have been made.
        assertEquals(null, fRetrieveRm);

        // The cache has no data so the query should have failed  
        try {
            q.get();
            fail("expected an exeption");
        } catch (ExecutionException e) {
            // expected the exception
        }
    }

	/**
	 * Test behavior when a cache object goes into the "disabled" state while an
	 * update request is ongoing. The subsequent completion of the request should 
	 * have no effect on the  cache
	 */
    @Test 
    public void disableWhilePendingTest() throws InterruptedException, ExecutionException {
        // Request data from cache
        Query<Integer> q = new TestQuery(); 
        fExecutor.execute(q);

        // Disable the cache
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fTestCache.set(null, STATUS_TARGET_RUNNING);
            }
        }).get();
        
        assertCacheValidWithoutData();

		// Complete the retrieve RM. Note that the disabling of the cache above
		// disassociates it from its retrieval RM. Thus regardless of how that
		// request completes, it does not affect the cache.
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fRetrieveRm.setData(1);
                fRetrieveRm.done();
            }
        }).get();
        
        // Validate that cache is still disabled without data.
        assertCacheValidWithoutData();
    }

	/**
	 * Test behavior when a cache object goes into the "disabled" state while
	 * it's in the valid state. The cache remains in the valid state but it
	 * loses its data and obtains an error status.
	 */
    @Test 
    public void disableWhileValidTest() throws InterruptedException, ExecutionException {
        // Request data from cache
        Query<Integer> q = new TestQuery(); 
        fExecutor.execute(q);
        
        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();
        
        // Complete the request
        fRetrieveRm.setData(1);
        fRetrieveRm.done();

        assertEquals(Integer.valueOf(1), q.get());
        
        // Disable the cache
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fTestCache.set(null, STATUS_TARGET_RUNNING);
            }
        }).get();
        
        // Check final state
        assertCacheValidWithoutData();
    }

    @Test 
    public void setWithValueTest() throws InterruptedException, ExecutionException {
        // Disable the cache
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fTestCache.set(2, Status.OK_STATUS);
            }
        }).get();
        
        // Validate that cache is disabled without data.
        assertCacheValidWithData(2);
    }
    
    
    @Test 
    public void cancelWhilePendingTest() throws InterruptedException, ExecutionException {
        // Request data from cache 
        Query<Integer> q = new TestQuery();
        fExecutor.execute(q);

        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();

        // Cancel the client request
        q.cancel(true);
        try {
            q.get();
            fail("Expected a cancellation exception");
        } catch (CancellationException e) {} // Expected exception;
        
        assertCacheInvalidAndWithCanceledRM();

		// Simulate the retrieval completing successfully despite the cancel
		// request. Perhaps the retrieval logic isn't checking the RM status.
		// Even if it is checking, it may have gotten passed its last checkpoint
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fRetrieveRm.setData(1);
                fRetrieveRm.done();
            }
        }).get();

        // Validate that cache didn't accept the result after its RM was canceled  
        assertCacheInvalidAndWithCanceledRM();
    }

    @Test 
    public void cancelWhilePendingTest2() throws InterruptedException, ExecutionException {
        // Request data from cache 
        Query<Integer> q = new TestQuery();
        fExecutor.execute(q);

        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();

        // Cancel the client request
        q.cancel(true);
        try {
            q.get();
            fail("Expected a cancellation exception");
        } catch (CancellationException e) {} // Expected exception;
        
        assertCacheInvalidAndWithCanceledRM();

		// Simulate retrieval logic that is regularly checking the RM's cancel
		// status and has discovered that the request has been canceled. It
		// technically does not need to explicitly set a cancel status object in
		// the RM, thanks to RequestMonitor.getStatus() automatically returning
		// Status.CANCEL_STATUS when its in the cancel state. So here we
		// simulate the retrieval logic just aborting its operations and
		// completing the RM. Note that it hasn't provided the data to the
		// cache. 
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fRetrieveRm.done();
            }
        }).get();

        assertCacheInvalidAndWithCanceledRM();
    }

    @Test 
    public void cancelWhilePendingTest3() throws InterruptedException, ExecutionException {
        // Request data from cache 
        Query<Integer> q = new TestQuery();
        fExecutor.execute(q);

        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();

        // Cancel the client request
        q.cancel(true);
        try {
            q.get();
            fail("Expected a cancellation exception");
        } catch (CancellationException e) {} // Expected exception;
        
        assertCacheInvalidAndWithCanceledRM();

		// Simulate retrieval logic that is regularly checking the RM's cancel
		// status and has discovered that the request has been canceled. It
		// aborts its processing, sets STATUS.CANCEL_STATUS in the RM and
		// completes it. 
        fExecutor.submit(new DsfRunnable() {
            public void run() {
            	fRetrieveRm.setStatus(Status.CANCEL_STATUS);
                fRetrieveRm.done();
            }
        }).get();

        // Validate that cache didn't accept the result after its RM was canceled
        assertCacheInvalidAndWithCanceledRM();
    }

	@Test 
    public void cancelWhilePendingWithoutClientNotificationTest() throws InterruptedException, ExecutionException {
	    final boolean canceledCalled[] = new boolean[] { false };
	    
	    fTestCache = new TestCache() {
	        protected synchronized void canceled() {
	            canceledCalled[0] = true;
	        };
	    };
	    
        // Request data from cache 
        Query<Integer> q = new Query<Integer>() { 
            @Override
            protected void execute(final DataRequestMonitor<Integer> rm) {
                
                fTestCache.update(new RequestMonitor(ImmediateExecutor.getInstance(), rm) {
                    @Override
                    public synchronized void addCancelListener(ICanceledListener listener) {
                        // Do not add the cancel listener so that the cancel request is not
                        // propagated to the cache.
                    }
                    
                    @Override
                    protected void handleSuccess() {
                        rm.setData(fTestCache.getData());
                        rm.done();
                    }
                });
            }
        };
        fExecutor.execute(q);

        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();

        // Cancel the client request
        q.cancel(true);
        
        assertCacheInvalidAndWithCanceledRM();
        
        // AbstractCache.canceled() should be called after isCanceled() 
        // discovers that the client has canceled its request.  The canceled() method is 
        // called in a separate dispatch cycle, so we have to wait one cycle of the executor 
        // after is canceled is called.
        fRetrieveRm.isCanceled();
        fExecutor.submit(new Runnable() { public void run() {} }).get(); 
        assertTrue(canceledCalled[0]);        

        try {
            q.get();
            fail("Expected a cancellation exception");
        } catch (CancellationException e) {} // Expected exception;


        // Completed the retrieve RM
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fRetrieveRm.setData(1);
                fRetrieveRm.done();
            }
        }).get();

        // Validate that cache didn't accept the result after its RM was canceled
        assertCacheInvalidAndWithCanceledRM();
    }

    /**
     * This test forces a race condition where a client that requested data 
     * cancels.  While shortly after a second client starts a new request.
     * The first request's cancel should not interfere with the second 
     * request.  
     */
    @Test 
    public void cancelAfterCompletedRaceCondition() throws InterruptedException, ExecutionException {

        // Create a client request with a badly behaved cancel implementation.
        final RequestMonitor[] rmBad = new RequestMonitor[1] ;
        final boolean qBadCanceled[] = new boolean[] { false };
        Query<Integer> qBad = new Query<Integer>() { 
            @Override
            protected void execute(final DataRequestMonitor<Integer> rm) {
                rmBad[0] = new RequestMonitor(ImmediateExecutor.getInstance(), rm) {
                    @Override
                    public synchronized void removeCancelListener(ICanceledListener listener) {
                        // Do not add the cancel listener so that the cancel request is not
                        // propagated to the cache.
                    }
                    
                    @Override
                    public void cancel() {
                        if (qBadCanceled[0]) {
                            super.cancel();
                        }
                    }
                    
                    @Override
                    public synchronized boolean isCanceled() {
                        return qBadCanceled[0];
                    }
                    
                    @Override
                    public synchronized void done() {
                        // Avoid clearing cancel listeners list
                    };
                    
                    @Override
					protected void handleSuccess() {
                        rm.setData(fTestCache.getData());
                        rm.done();
                    };
                };
                
                fTestCache.update(rmBad[0]);
            }
        };
        fExecutor.execute(qBad);

        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();

        // Reset the cache
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fRetrieveRm = null;
                fTestCache.set(null, Status.OK_STATUS);
                fTestCache.reset();
            }
        }).get();
        
        Query<Integer> qGood = new TestQuery();
        fExecutor.execute(qGood);

        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();
        
        qBadCanceled[0] = true;
        rmBad[0].cancel();

        assertFalse(fRetrieveRm.isCanceled());
        
        // Completed the retrieve RM
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fRetrieveRm.setData(1);
                fRetrieveRm.done();
            }
        }).get();

        qGood.get();
        
        assertCacheValidWithData(1);
    }
    
    @Test 
    public void cancelWhilePendingWithTwoClientsTest() throws InterruptedException, ExecutionException {
    	
		// Request data from cache. Use submit/get to ensure both update
		// requests are initiated before we wait for retrieval to start
        Query<Integer> q1 = new TestQuery();
        fExecutor.submit(q1).get();   

        // Request data from cache again
        Query<Integer> q2 = new TestQuery();
        fExecutor.submit(q2).get();
        

        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();

        // Cancel the first client request
        q1.cancel(true);
        try {
            q1.get();
            fail("Expected a cancellation exception");
        } catch (CancellationException e) {} // Expected exception;
        assertCacheWaiting();

        // Cancel the second request
        q2.cancel(true);
        try {
            q2.get();
            fail("Expected a cancellation exception");
        } catch (CancellationException e) {} // Expected exception;

        assertCacheInvalidAndWithCanceledRM();

        // Completed the retrieve RM
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fRetrieveRm.setData(1);
                fRetrieveRm.done();
            }
        }).get();

        // Validate that cache didn't accept the result after its RM was canceled
        assertCacheInvalidAndWithCanceledRM();
    }

    @Test 
    public void cancelWhilePendingWithManyClientsTest() throws InterruptedException, ExecutionException {
        // Request data from cache 
        List<Query<Integer>> qList = new ArrayList<Query<Integer>>(); 
        for (int i = 0; i < 10; i++) {
            Query<Integer> q = new TestQuery();
            fExecutor.submit(q).get();
            qList.add(q);
        }

        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();

        // Cancel some client requests
        int[] toCancel = new int[] { 0, 2, 5, 9};
        for (int i = 0; i < toCancel.length; i++) {
            
            // Cancel request and verify that its canceled
            Query<Integer> q = qList.get(toCancel[i]);
            q.cancel(true);
            try {
                q.get();
                fail("Expected a cancellation exception");
            } catch (CancellationException e) {} // Expected exception;
            qList.set(toCancel[i], null);
            
            assertCacheWaiting();
        }

        // Replace canceled requests with new ones
        for (int i = 0; i < toCancel.length; i++) {
            Query<Integer> q = new TestQuery();
            fExecutor.submit(q).get();
            qList.set(toCancel[i], q);
            assertCacheWaiting();
        }

        // Now cancel all requests
        for (int i = 0; i < (qList.size() - 1); i++) {
            // Validate that cache is still waiting and is not canceled
            assertCacheWaiting();
            qList.get(i).cancel(true);
        }
        qList.get(qList.size() - 1).cancel(true);
        assertCacheInvalidAndWithCanceledRM();

        // Completed the retrieve RM
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fRetrieveRm.setData(1);
                fRetrieveRm.done();
            }
        }).get();

        // Validate that cache didn't accept the result after its RM was canceled
        assertCacheInvalidAndWithCanceledRM();
    }

    @Test 
    public void resetWhileValidTest() throws InterruptedException, ExecutionException {
        // Request data from cache
        Query<Integer> q = new TestQuery();
        fExecutor.execute(q);

        // Wait until the cache starts data retrieval.
        waitForRetrieveRm();
        
        // Complete the request
        fRetrieveRm.setData(1);
        fRetrieveRm.done();

        q.get();
        
        // Disable cache
        fExecutor.submit(new DsfRunnable() {
            public void run() {
                fTestCache.reset();
            }
        }).get();
        
        // Check final state
        assertCacheResetWithoutData();
    }
}