/* ###
 * IP: GHIDRA
 *
 * Licensed 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 ghidra.util.task;

import static org.junit.Assert.*;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.junit.Before;
import org.junit.Test;

import generic.test.AbstractGuiTest;
import ghidra.util.SystemUtilities;

public class BufferedSwingRunnerTest extends AbstractGuiTest {

	private static final int MIN_DELAY = 500;
	private static final int MAX_DELAY = 1000;
	private volatile int runnableCalled;
	private BufferedSwingRunner runner = new BufferedSwingRunner(MIN_DELAY, MAX_DELAY);
	private ClientRunnable runnable = new ClientRunnable();

	@Before
	public void setUp() throws Exception {
		// must turn this on to get the expected results, as in headless mode the update manager
		// will run it's Swing work immediately on the test thread, which is not true to the
		// default behavior
		System.setProperty(SystemUtilities.HEADLESS_PROPERTY, Boolean.FALSE.toString());
	}

	@Test
	public void testOneCallForOneRun() {
		runner.run(runnable);
		waitForRunner();
		assertEquals("Expected only 1 callback", 1, runnableCalled);
	}

	@Test
	public void testTwoCallForMultipleFastRunCalls() {
		for (int i = 0; i < 10; i++) {
			runner.run(runnable);
			sleep(10);
		}
		waitForRunner();
		assertEquals("Expected 2 callbacks", 2, runnableCalled);
	}

	@Test
	public void testOneCallForRunLater() {
		runner.runLater(runnable);
		waitForRunner();
		assertEquals("Expected only 1 callback", 1, runnableCalled);
	}

	@Test
	public void testMaxTimeout() {

		//
		// Hard Test!: Many timing-sensitive factors are at play here: this thread and it's
		//             run and sleep scheduling; the Swing thread and its scheduling and the
		//             Swing timer's scheduling.  Thus, making a simple AND reliable test is
		//             nearly impossible.  So, we test here the basic max timeout mechanism.
		//
		//             The loop below calls update many times.  The first call to update always
		//             triggers an immediate update.  Each successive call, as long as it is
		//             faster than *min delay* will trigger an update to be buffered, to be
		//             delayed.  This delay gets reset upon each update call until the *max delay*
		//             time has been passed.
		//

		int sleepyTime = 5;
		int maxDelay = 600;
		runner = new BufferedSwingRunner(200, maxDelay);

		for (int i = 0; i < 4; i++) {
			runner.run(runnable);
			sleep(sleepyTime);
		}

		assertEquals("Expected 1 max delay callback", 1, runnableCalled);

		waitForRunner();
		assertEquals("Expected one immediate callback and one max delay callback (2 total)", 2,
			runnableCalled);
	}

	@Test
	public void testFlush() {
		runner.flush();
		waitForSwing();
		assertFalse(runner.isBusy());
		assertEquals("Did not expect the callback after stop()", 0, runnableCalled);

		runner.runLater(runnable);
		runner.flush();
		waitForRunner();
		assertEquals("Expected only 1 callback", 1, runnableCalled);
	}

	@Test
	public void testStop() {
		runner.runLater(runnable);
		runner.stop();
		waitForSwing();
		assertFalse(runner.isBusy());
		assertEquals("Did not expect the callback after stop()", 0, runnableCalled);

		//
		// Make sure we can use again
		//
		runnableCalled = 0;
		runner.run(runnable);
		waitForRunner();
		assertEquals("Expected only 1 callback", 1, runnableCalled);
	}

	@Test
	public void testHasPendingUpdates() {
		runner.runLater(runnable);
		assertTrue("Should have pending updates after calling update, but before the work" +
			"has been done", runner.hasPendingUpdates());
		waitForRunner();
		assertEquals("Expected only 1 callback", 1, runnableCalled);
		assertFalse("Still have pending updates after performing work",
			runner.hasPendingUpdates());
	}

	@Test
	public void testDispose() {
		for (int i = 0; i < 10; i++) {
			runner.run(runnable);
			sleep(1);
		}
		assertTrue(runner.hasPendingUpdates());
		runner.dispose();
		assertFalse(runner.hasPendingUpdates());
		int called = runnableCalled;
		//
		// Cannot use again
		//
		runner.run(runnable);
		waitForRunner();
		// make sure the callback did not occur.
		assertEquals(called, runnableCalled);
	}

	@Test
	public void testIsBusy() throws Exception {
		//
		// Another complicated test: we want to make sure that if we try to trigger an update
		// that isBusy() will return true, even if the update runnable has not yet been posted
		// to the Swing thread.
		//
		// To do this, we will need to block the Swing thread so that we know the request happens
		// before the update runnable is called.
		//

		CountDownLatch startLatch = new CountDownLatch(1);
		CountDownLatch endLatch = new CountDownLatch(1);
		AtomicBoolean exception = new AtomicBoolean();

		runSwing(new Runnable() {
			@Override
			public void run() {
				startLatch.countDown();
				try {
					endLatch.await(10, TimeUnit.SECONDS);
				}
				catch (InterruptedException e) {
					exception.set(true);
				}
			}
		}, false);

		// This will cause the swing thread to block until we countdown the end latch
		startLatch.await(10, TimeUnit.SECONDS);

		runner.run(runnable);
		assertTrue("Manager not busy after requesting an update", runner.isBusy());

		endLatch.countDown();

		waitForRunner();
		assertFalse("Manager still busy after waiting for update", runner.isBusy());

		assertFalse("Interrupted waiting for CountDowLatch", exception.get());
	}

	@Test
	public void testCallToUpdateWhileAnUpdateIsWorking() throws Exception {

		//
		// Test that an update call from a non-swing thread will still get processed if the
		// manager is actively processing an update on the swing thread.
		//

		CountDownLatch startLatch = new CountDownLatch(1);
		CountDownLatch endLatch = new CountDownLatch(1);
		AtomicBoolean exception = new AtomicBoolean();

		Runnable r = () -> {
			runnableCalled++;

			startLatch.countDown();
			try {
				endLatch.await(10, TimeUnit.SECONDS);
			}
			catch (InterruptedException e) {
				exception.set(true);
			}
		};

		// start the update manager and have it wait for us
		runner.run(r);

		// have the swing thread block until we countdown the end latch
		startLatch.await(10, TimeUnit.SECONDS);

		// post the second update request now that the manager is actively processing
		runner.run(runnable);

		// let the update manager finish; verify 2 work items total
		endLatch.countDown();
		waitForRunner();
		assertEquals("Expected exactly 2 callbacks", 2, runnableCalled);
	}

	@Test
	public void testNotFiringTooOften() throws InterruptedException {
		Thread t = new Thread(() -> {
			for (int i = 0; i < 50; i++) {
				runner.run(runnable);
				runner.run(runnable);
				runner.run(runnable);
				runner.run(runnable);
				sleep(10);
			}
		});
		t.start();
		t.join();
		waitForRunner();
		assertEquals(2, runnableCalled);
	}

//==================================================================================================
// Private Methods
//==================================================================================================
	private void waitForRunner() {

		// let all swing updates finish, which may trigger the update manager
		waitForSwing();

		while (runner.isBusy()) {
			sleep(DEFAULT_WAIT_DELAY);
		}

		// let any resulting swing events finish
		waitForSwing();
	}

	private class ClientRunnable implements Runnable {
		@Override
		public void run() {
			runnableCalled++;
		}
	}
}
