/*
 * Hibernate Search, full-text search for your domain model
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */

package org.hibernate.search.testsupport;

import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;

import org.jboss.byteman.rule.Rule;
import org.jboss.byteman.rule.exception.ThrowException;
import org.jboss.byteman.rule.helper.Helper;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

import static org.junit.Assert.fail;

/**
 * @author Sanne Grinovero (C) 2011 Red Hat Inc.
 * @author Hardy Ferentschik
 */
public class BytemanHelper extends Helper {

	private static final Log log = LoggerFactory.make();

	private static final AtomicInteger counter = new AtomicInteger();
	private static final Deque<String> concurrentStack = new ConcurrentLinkedDeque<>();

	protected BytemanHelper(Rule rule) {
		super( rule );
	}

	public void sleepASecond() {
		try {
			log.info( "Byteman rule triggered: sleeping a second" );
			Thread.sleep( 1000 );
		}
		catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			log.error( "unexpected interruption", e );
		}
	}

	public void assertBooleanValue(boolean actual, boolean expected) {
		if ( actual != expected ) {
			fail( "Unexpected boolean value" );
		}
	}

	public void countInvocation() {
		log.debug( "Increment call count" );
		counter.incrementAndGet();
	}

	/**
	 * Throws an exception of a type that is not accessible in the wrapped method.
	 * <p>
	 * Note that the byteman "throw" keyword only works with exception types
	 * imported by the class whose method is being wrapped.
	 */
	public void simulateFailure() {
		throw new ThrowException( new SimulatedFailureException() );
	}

	/**
	 * Throws an exception of a type that is not accessible in the wrapped method.
	 * <p>
	 * Note that the byteman "throw" keyword only works with exception types
	 * imported by the class whose method is being wrapped.
	 *
	 * @param message The message carried by the exception.
	 */
	public void simulateFailure(String message) {
		throw new ThrowException( new SimulatedFailureException( message ) );
	}

	/**
	 * Adds a label to a concurrent queue.
	 * Useful to "tag" events to verify they are issued in a specific order,
	 * even though they are generated by different threads.
	 * @param message some label to be recorded
	 */
	public void pushEvent(String message) {
		concurrentStack.add( message );
	}

	/**
	 * @return A utility to be referenced in the test as a {@link org.junit.Rule JUnit rule}, that allows
	 * access to information resulting from Byteman rules.
	 */
	public static BytemanAccessor createAccessor() {
		return new BytemanAccessor();
	}

	public static class BytemanAccessor implements TestRule {

		/*
		 * We use this attribute to check that the accessor is being used
		 * as a JUnit rule (as it should be).
		 */
		private boolean runningAsJUnitRule = false;

		private BytemanAccessor() {
			// Private constructor, use createAccessor() instead.
		}

		public boolean isEventStackEmpty() {
			ensureRunningAsJUnitRule();
			return concurrentStack.isEmpty();
		}

		/**
		 * Gets the first event label from the concurrent queue,
		 * and removes it.
		 * The next invocation will return the next one from the queue.
		 * @return the first
		 */
		public String consumeNextRecordedEvent() {
			ensureRunningAsJUnitRule();
			try {
				return concurrentStack.removeFirst();
			}
			catch (java.util.NoSuchElementException nse) {
				throw new IllegalStateException( "Attempted to consume an event, while no events are left on the stack."
						+ " If it is the first call to this method, check if the Byteman rules are actually being triggered?" );
			}
		}

		public int getAndResetInvocationCount() {
			ensureRunningAsJUnitRule();
			return counter.getAndSet( 0 );
		}

		private void ensureRunningAsJUnitRule() {
			if ( ! runningAsJUnitRule ) {
				throw new IllegalStateException( "Error in test setup: the byteman accessor obtained"
						+ " through BytemanHelper.createAccessor() must be used as a JUnit Rule (see org.junit.Rule)." );
			}
		}

		@Override
		public Statement apply(final Statement base, Description description) {
			return new Statement() {
				@Override
				public void evaluate() throws Throwable {
					runningAsJUnitRule = true;
					try {
						base.evaluate();
					}
					finally {
						runningAsJUnitRule = false;
						reset();
					}
				}
			};
		}

		private void reset() {
			concurrentStack.clear();
			counter.set( 0 );
		}
	}

	public static class SimulatedFailureException extends RuntimeException {
		private SimulatedFailureException() {
		}

		private SimulatedFailureException(String message) {
			super( message );
		}
	}
}
