/*******************************************************************************
 * Copyright (c) 2003, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM - Initial API and implementation
 *******************************************************************************/
package org.eclipse.core.tests.runtime.jobs;

import java.util.ArrayList;
import junit.framework.TestCase;
import org.eclipse.core.internal.jobs.LockManager;
import org.eclipse.core.internal.jobs.OrderedLock;
import org.eclipse.core.runtime.jobs.ILock;
import org.eclipse.core.runtime.jobs.LockListener;
import org.eclipse.core.tests.harness.TestBarrier;

/**
 * Tests implementation of ILock objects
 */
public class OrderedLockTest extends TestCase {
	public OrderedLockTest() {
		super(null);
	}

	public OrderedLockTest(String name) {
		super(name);
	}

	/**
	 * Creates n runnables on the given lock and adds them to the given list.
	 */
	private void createRunnables(ILock[] locks, int n, ArrayList<LockAcquiringRunnable> allRunnables) {
		for (int i = 0; i < n; i++)
			allRunnables.add(new LockAcquiringRunnable(locks));
	}

	private void kill(ArrayList<LockAcquiringRunnable> allRunnables) {
		for (LockAcquiringRunnable lockAcquiringRunnable : allRunnables) {
			lockAcquiringRunnable.kill();
		}
	}

	public void testComplex() {
		ArrayList<LockAcquiringRunnable> allRunnables = new ArrayList<>();
		LockManager manager = new LockManager();
		OrderedLock lock1 = manager.newLock();
		OrderedLock lock2 = manager.newLock();
		OrderedLock lock3 = manager.newLock();
		createRunnables(new ILock[] {lock1, lock2, lock3}, 5, allRunnables);
		createRunnables(new ILock[] {lock3, lock2, lock1}, 5, allRunnables);
		createRunnables(new ILock[] {lock1, lock3, lock2}, 5, allRunnables);
		createRunnables(new ILock[] {lock2, lock3, lock1}, 5, allRunnables);
		start(allRunnables);
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
		}
		kill(allRunnables);
		for (int i = 0; i < allRunnables.size(); i++) {
			allRunnables.get(i).isDone();
		}
		//the underlying array has to be empty
		assertTrue("Locks not removed from graph.", manager.isEmpty());
	}

	public void testSimple() {
		ArrayList<LockAcquiringRunnable> allRunnables = new ArrayList<>();
		LockManager manager = new LockManager();
		OrderedLock lock1 = manager.newLock();
		OrderedLock lock2 = manager.newLock();
		OrderedLock lock3 = manager.newLock();
		createRunnables(new ILock[] {lock1, lock2, lock3}, 1, allRunnables);
		createRunnables(new ILock[] {lock3, lock2, lock1}, 1, allRunnables);
		start(allRunnables);
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
		}
		kill(allRunnables);
		for (int i = 0; i < allRunnables.size(); i++) {
			allRunnables.get(i).isDone();
		}
		//the underlying array has to be empty
		assertTrue("Locks not removed from graph.", manager.isEmpty());
	}

	public void testLockAcquireInterrupt() throws InterruptedException {
		final TestBarrier barrier = new TestBarrier();
		LockManager manager = new LockManager();
		final ILock lock = manager.newLock();
		final boolean[] wasInterupted = new boolean[] {false};
		Thread t = new Thread() {
			@Override
			public void run() {
				barrier.setStatus(TestBarrier.STATUS_RUNNING);
				barrier.waitForStatus(TestBarrier.STATUS_WAIT_FOR_START);
				barrier.setStatus(TestBarrier.STATUS_WAIT_FOR_DONE);
				lock.acquire();
				wasInterupted[0] = Thread.currentThread().isInterrupted();
				lock.release();
			}
		};
		//schedule and wait for thread to start
		t.start();
		barrier.waitForStatus(TestBarrier.STATUS_RUNNING);
		//acquire the lock and let the thread proceed
		lock.acquire();
		barrier.setStatus(TestBarrier.STATUS_WAIT_FOR_START);
		//make sure thread is blocked
		barrier.waitForStatus(TestBarrier.STATUS_WAIT_FOR_DONE);
		Thread.sleep(500);
		t.interrupt();
		lock.release();
		t.join();
		assertTrue("1.0", wasInterupted[0]);
	}

	/**
	 * test that an acquire call that times out does not
	 * become the lock owner (regression test)
	 */
	public void testLockTimeout() {
		//create a new lock manager and 1 lock
		final LockManager manager = new LockManager();
		final OrderedLock lock = manager.newLock();
		//status array for communicating between threads
		final int[] status = {TestBarrier.STATUS_START};
		//array to end a runnable after it is no longer needed
		final boolean[] alive = {true};

		//first runnable which is going to hold the created lock
		Runnable getLock = () -> {
			lock.acquire();
			status[0] = TestBarrier.STATUS_RUNNING;
			while (alive[0]) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
				}
			}
			lock.release();
			status[0] = TestBarrier.STATUS_DONE;
		};

		//second runnable which is going to try and acquire the given lock and then time out
		Runnable tryForLock = () -> {
			boolean success = false;
			try {
				success = lock.acquire(100);
			} catch (InterruptedException e) {
			}
			assertTrue("1.0", !success);
			assertTrue("1.1", !manager.isLockOwner());
			status[0] = TestBarrier.STATUS_WAIT_FOR_DONE;
		};

		Thread first = new Thread(getLock);
		Thread second = new Thread(tryForLock);

		//start the first thread and wait for it to acquire the lock
		first.start();
		TestBarrier.waitForStatus(status, TestBarrier.STATUS_RUNNING);
		//start the second thread, make sure the assertion passes
		second.start();
		TestBarrier.waitForStatus(status, TestBarrier.STATUS_WAIT_FOR_DONE);
		//let the first thread die
		alive[0] = false;
		TestBarrier.waitForStatus(status, TestBarrier.STATUS_DONE);
		//the underlying array has to be empty
		assertTrue("Locks not removed from graph.", manager.isEmpty());
	}

	/**
	 * test that when a Lock Listener forces the Lock Manager to grant a lock
	 * to a waiting thread, that other threads in the queue don't get disposed (regression test)
	 */
	public void testLockRequestDisappearence() {
		//create a new lock manager and 1 lock
		final LockManager manager = new LockManager();
		final OrderedLock lock = manager.newLock();
		//status array for communicating between threads
		final int[] status = {TestBarrier.STATUS_WAIT_FOR_START, TestBarrier.STATUS_WAIT_FOR_START};

		//first runnable which is going to hold the created lock
		Runnable getLock = () -> {
			lock.acquire();
			status[0] = TestBarrier.STATUS_START;
			TestBarrier.waitForStatus(status, 0, TestBarrier.STATUS_RUNNING);
			lock.release();
			status[0] = TestBarrier.STATUS_DONE;
		};

		//second runnable which is going to submit a request for this lock and wait until it is available
		Runnable waitForLock = () -> {
			status[1] = TestBarrier.STATUS_START;
			lock.acquire();
			assertTrue("1.0", manager.isLockOwner());
			lock.release();
			status[1] = TestBarrier.STATUS_DONE;

		};

		//third runnable which is going to submit a request for this lock but not wait
		//because the hook is going to force it to be given the lock (implicitly)
		Runnable forceGetLock = () -> {
			lock.acquire();
			lock.release();
			status[0] = TestBarrier.STATUS_WAIT_FOR_DONE;
		};

		//a locklistener to force lock manager to give the lock to the third runnable (implicitly)
		LockListener listener = new LockListener() {
			@Override
			public boolean aboutToWait(Thread lockOwner) {
				return true;
			}
		};

		//assign each runnable to a separate thread
		Thread first = new Thread(getLock);
		Thread second = new Thread(waitForLock);
		Thread third = new Thread(forceGetLock);

		//start the first thread and wait for it to acquire the lock
		first.start();
		TestBarrier.waitForStatus(status, 0, TestBarrier.STATUS_START);
		//start the second thread, make sure it is added to the lock wait queue
		second.start();
		TestBarrier.waitForStatus(status, 1, TestBarrier.STATUS_START);

		//assign our listener to the manager
		manager.setLockListener(listener);
		//start the third thread
		third.start();
		TestBarrier.waitForStatus(status, 0, TestBarrier.STATUS_WAIT_FOR_DONE);

		//let the first runnable complete
		status[0] = TestBarrier.STATUS_RUNNING;
		TestBarrier.waitForStatus(status, 0, TestBarrier.STATUS_DONE);

		//now wait for the second runnable to get the lock, and have the assertion pass
		TestBarrier.waitForStatus(status, 1, TestBarrier.STATUS_DONE);

		//the underlying array has to be empty
		assertTrue("Locks not removed from graph.", manager.isEmpty());
	}

	private void start(ArrayList<LockAcquiringRunnable> allRunnables) {
		for (LockAcquiringRunnable lockAcquiringRunnable : allRunnables) {
			new Thread(lockAcquiringRunnable).start();
		}
	}
}
