/*
 * Copyright 2002-2014 the original author or authors.
 *
 * 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
 *
 *      https://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 org.springframework.messaging.support;

import java.util.concurrent.atomic.AtomicInteger;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.springframework.core.task.TaskExecutor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandler;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;

/**
 * Unit tests for {@link ExecutorSubscribableChannel}.
 *
 * @author Phillip Webb
 */
public class ExecutorSubscribableChannelTests {

	@Rule
	public ExpectedException thrown = ExpectedException.none();

	private ExecutorSubscribableChannel channel = new ExecutorSubscribableChannel();

	@Mock
	private MessageHandler handler;

	private final Object payload = new Object();

	private final Message<Object> message = MessageBuilder.withPayload(this.payload).build();

	@Captor
	private ArgumentCaptor<Runnable> runnableCaptor;


	@Before
	public void setup() {
		MockitoAnnotations.initMocks(this);
	}

	@Test
	public void messageMustNotBeNull() throws Exception {
		thrown.expect(IllegalArgumentException.class);
		thrown.expectMessage("Message must not be null");
		this.channel.send(null);
	}

	@Test
	public void sendWithoutExecutor() {
		BeforeHandleInterceptor interceptor = new BeforeHandleInterceptor();
		this.channel.addInterceptor(interceptor);
		this.channel.subscribe(this.handler);
		this.channel.send(this.message);
		verify(this.handler).handleMessage(this.message);
		assertEquals(1, interceptor.getCounter().get());
		assertTrue(interceptor.wasAfterHandledInvoked());
	}

	@Test
	public void sendWithExecutor() throws Exception {
		BeforeHandleInterceptor interceptor = new BeforeHandleInterceptor();
		TaskExecutor executor = mock(TaskExecutor.class);
		ExecutorSubscribableChannel testChannel = new ExecutorSubscribableChannel(executor);
		testChannel.addInterceptor(interceptor);
		testChannel.subscribe(this.handler);
		testChannel.send(this.message);
		verify(executor).execute(this.runnableCaptor.capture());
		verify(this.handler, never()).handleMessage(this.message);
		this.runnableCaptor.getValue().run();
		verify(this.handler).handleMessage(this.message);
		assertEquals(1, interceptor.getCounter().get());
		assertTrue(interceptor.wasAfterHandledInvoked());
	}

	@Test
	public void subscribeTwice() throws Exception {
		assertThat(this.channel.subscribe(this.handler), equalTo(true));
		assertThat(this.channel.subscribe(this.handler), equalTo(false));
		this.channel.send(this.message);
		verify(this.handler, times(1)).handleMessage(this.message);
	}

	@Test
	public void unsubscribeTwice() throws Exception {
		this.channel.subscribe(this.handler);
		assertThat(this.channel.unsubscribe(this.handler), equalTo(true));
		assertThat(this.channel.unsubscribe(this.handler), equalTo(false));
		this.channel.send(this.message);
		verify(this.handler, never()).handleMessage(this.message);
	}

	@Test
	public void failurePropagates() throws Exception {
		RuntimeException ex = new RuntimeException();
		willThrow(ex).given(this.handler).handleMessage(this.message);
		MessageHandler secondHandler = mock(MessageHandler.class);
		this.channel.subscribe(this.handler);
		this.channel.subscribe(secondHandler);
		try {
			this.channel.send(message);
		}
		catch (MessageDeliveryException actualException) {
			assertThat(actualException.getCause(), equalTo(ex));
		}
		verifyZeroInteractions(secondHandler);
	}

	@Test
	public void concurrentModification() throws Exception {
		this.channel.subscribe(message1 -> channel.unsubscribe(handler));
		this.channel.subscribe(this.handler);
		this.channel.send(this.message);
		verify(this.handler).handleMessage(this.message);
	}

	@Test
	public void interceptorWithModifiedMessage() {
		Message<?> expected = mock(Message.class);
		BeforeHandleInterceptor interceptor = new BeforeHandleInterceptor();
		interceptor.setMessageToReturn(expected);
		this.channel.addInterceptor(interceptor);
		this.channel.subscribe(this.handler);
		this.channel.send(this.message);
		verify(this.handler).handleMessage(expected);
		assertEquals(1, interceptor.getCounter().get());
		assertTrue(interceptor.wasAfterHandledInvoked());
	}

	@Test
	public void interceptorWithNull() {
		BeforeHandleInterceptor interceptor1 = new BeforeHandleInterceptor();
		NullReturningBeforeHandleInterceptor interceptor2 = new NullReturningBeforeHandleInterceptor();
		this.channel.addInterceptor(interceptor1);
		this.channel.addInterceptor(interceptor2);
		this.channel.subscribe(this.handler);
		this.channel.send(this.message);
		verifyNoMoreInteractions(this.handler);
		assertEquals(1, interceptor1.getCounter().get());
		assertEquals(1, interceptor2.getCounter().get());
		assertTrue(interceptor1.wasAfterHandledInvoked());
	}

	@Test
	public void interceptorWithException() {
		IllegalStateException expected = new IllegalStateException("Fake exception");
		willThrow(expected).given(this.handler).handleMessage(this.message);
		BeforeHandleInterceptor interceptor = new BeforeHandleInterceptor();
		this.channel.addInterceptor(interceptor);
		this.channel.subscribe(this.handler);
		try {
			this.channel.send(this.message);
		}
		catch (MessageDeliveryException actual) {
			assertSame(expected, actual.getCause());
		}
		verify(this.handler).handleMessage(this.message);
		assertEquals(1, interceptor.getCounter().get());
		assertTrue(interceptor.wasAfterHandledInvoked());
	}


	private abstract static class AbstractTestInterceptor extends ChannelInterceptorAdapter
			implements ExecutorChannelInterceptor {

		private AtomicInteger counter = new AtomicInteger();

		private volatile boolean afterHandledInvoked;


		public AtomicInteger getCounter() {
			return this.counter;
		}

		public boolean wasAfterHandledInvoked() {
			return this.afterHandledInvoked;
		}

		@Override
		public Message<?> beforeHandle(Message<?> message, MessageChannel channel, MessageHandler handler) {
			assertNotNull(message);
			counter.incrementAndGet();
			return message;
		}

		@Override
		public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler, Exception ex) {
			this.afterHandledInvoked = true;
		}
	}

	private static class BeforeHandleInterceptor extends AbstractTestInterceptor {

		private Message<?> messageToReturn;

		private RuntimeException exceptionToRaise;


		public void setMessageToReturn(Message<?> messageToReturn) {
			this.messageToReturn = messageToReturn;
		}

		// TODO Determine why setExceptionToRaise() is unused.
		@SuppressWarnings("unused")
		public void setExceptionToRaise(RuntimeException exception) {
			this.exceptionToRaise = exception;
		}

		@Override
		public Message<?> beforeHandle(Message<?> message, MessageChannel channel, MessageHandler handler) {
			super.beforeHandle(message, channel, handler);
			if (this.exceptionToRaise != null) {
				throw this.exceptionToRaise;
			}
			return (this.messageToReturn != null ? this.messageToReturn : message);
		}
	}

	private static class NullReturningBeforeHandleInterceptor extends AbstractTestInterceptor {

		@Override
		public Message<?> beforeHandle(Message<?> message, MessageChannel channel, MessageHandler handler) {
			super.beforeHandle(message, channel, handler);
			return null;
		}
	}

}
