/*
 * Copyright 2013-2019 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 io.awspring.cloud.messaging.listener;

import java.lang.reflect.Method;

import io.awspring.cloud.core.support.documentation.RuntimeUse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.core.DestinationResolvingMessageSendingOperations;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.mock.env.MockPropertySource;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

/**
 * @author Alain Sahli
 * @author Agim Emruli
 */
@ExtendWith(MockitoExtension.class)
class SendToHandlerMethodReturnValueHandlerTest {

	@Mock
	private DestinationResolvingMessageSendingOperations<?> messageTemplate;

	@Test
	void supportsReturnType_methodAnnotatedWithSendTo_trueIsReturned() throws Exception {
		// Arrange
		SendToHandlerMethodReturnValueHandler sendToHandlerMethodReturnValueHandler;
		sendToHandlerMethodReturnValueHandler = new SendToHandlerMethodReturnValueHandler(null);
		Method validSendToMethod = this.getClass().getDeclaredMethod("validSendToMethod");
		MethodParameter methodParameter = new MethodParameter(validSendToMethod, -1);

		// Act
		boolean supports = sendToHandlerMethodReturnValueHandler.supportsReturnType(methodParameter);

		// Assert
		assertThat(supports).isTrue();
	}

	@Test
	void supportsReturnType_methodWithoutSendToAnnotation_falseIsReturned() throws Exception {
		// Arrange
		SendToHandlerMethodReturnValueHandler sendToHandlerMethodReturnValueHandler;
		sendToHandlerMethodReturnValueHandler = new SendToHandlerMethodReturnValueHandler(null);
		Method invalidSendToMethod = this.getClass().getDeclaredMethod("invalidSendToMethod");
		MethodParameter methodParameter = new MethodParameter(invalidSendToMethod, -1);

		// Act
		boolean supports = sendToHandlerMethodReturnValueHandler.supportsReturnType(methodParameter);

		// Assert
		assertThat(supports).isFalse();
	}

	@Test
	void supportsReturnType_methodWithSendToAnnotationWithoutValue_trueIsReturned() throws Exception {
		// Arrange
		SendToHandlerMethodReturnValueHandler sendToHandlerMethodReturnValueHandler;
		sendToHandlerMethodReturnValueHandler = new SendToHandlerMethodReturnValueHandler(null);
		Method validSendToMethod = this.getClass().getDeclaredMethod("anotherValidSendToMethod");
		MethodParameter methodParameter = new MethodParameter(validSendToMethod, -1);

		// Act
		boolean supports = sendToHandlerMethodReturnValueHandler.supportsReturnType(methodParameter);

		// Assert
		assertThat(supports).isTrue();
	}

	@Test
	void handleReturnValue_withNullMessageTemplate_exceptionIsThrown() throws Exception {
		// Arrange
		Method validSendToMethod = this.getClass().getDeclaredMethod("validSendToMethod");
		MethodParameter methodParameter = new MethodParameter(validSendToMethod, -1);
		SendToHandlerMethodReturnValueHandler sendToHandlerMethodReturnValueHandler;
		sendToHandlerMethodReturnValueHandler = new SendToHandlerMethodReturnValueHandler(null);

		// Assert
		assertThatThrownBy(() -> sendToHandlerMethodReturnValueHandler.handleReturnValue("Return me!", methodParameter,
				MessageBuilder.withPayload("Nothing").build())).isInstanceOf(IllegalStateException.class)
						.hasMessage("A messageTemplate must be set to handle the return value.");
	}

	@Test
	void handleReturnValue_withNullReturnValue_NoMessageTemplateIsCalled() throws Exception {
		// Arrange
		Method validSendToMethod = this.getClass().getDeclaredMethod("validSendToMethod");
		MethodParameter methodParameter = new MethodParameter(validSendToMethod, -1);
		SendToHandlerMethodReturnValueHandler sendToHandlerMethodReturnValueHandler;
		sendToHandlerMethodReturnValueHandler = new SendToHandlerMethodReturnValueHandler(this.messageTemplate);

		// Act
		sendToHandlerMethodReturnValueHandler.handleReturnValue(null, methodParameter,
				MessageBuilder.withPayload("Nothing").build());

		// Assert
		verify(this.messageTemplate, times(0)).convertAndSend(anyString(), anyString());
	}

	@Test
	void handleReturnValue_withAMessageTemplateAndAValidMethodWithDestination_templateIsCalled() throws Exception {
		// Arrange
		Method validSendToMethod = this.getClass().getDeclaredMethod("validSendToMethod");
		MethodParameter methodParameter = new MethodParameter(validSendToMethod, -1);
		SendToHandlerMethodReturnValueHandler sendToHandlerMethodReturnValueHandler;
		sendToHandlerMethodReturnValueHandler = new SendToHandlerMethodReturnValueHandler(this.messageTemplate);

		// Act
		sendToHandlerMethodReturnValueHandler.handleReturnValue("Elastic Hello!", methodParameter,
				MessageBuilder.withPayload("Nothing").build());

		// Assert
		verify(this.messageTemplate, times(1)).convertAndSend(eq("testQueue"), eq("Elastic Hello!"));
	}

	@Test
	void handleReturnValue_withExpressionInSendToName_templateIsCalled() throws Exception {
		// Arrange
		Method validSendToMethod = this.getClass().getDeclaredMethod("expressionMethod");
		MethodParameter methodParameter = new MethodParameter(validSendToMethod, -1);

		GenericApplicationContext applicationContext = new GenericApplicationContext();
		MockPropertySource propertySource = new MockPropertySource();
		propertySource.setProperty("queueName", "myTestQueue");

		applicationContext.getEnvironment().getPropertySources().addLast(propertySource);
		applicationContext.refresh();

		SendToHandlerMethodReturnValueHandler sendToHandlerMethodReturnValueHandler;
		sendToHandlerMethodReturnValueHandler = new SendToHandlerMethodReturnValueHandler(this.messageTemplate);
		sendToHandlerMethodReturnValueHandler.setBeanFactory(applicationContext.getAutowireCapableBeanFactory());

		// Act
		sendToHandlerMethodReturnValueHandler.handleReturnValue("expression method", methodParameter,
				MessageBuilder.withPayload("Nothing").build());

		// Assert
		verify(this.messageTemplate, times(1)).convertAndSend(eq("myTestQueue"), eq("expression method"));
	}

	@Test
	void handleReturnValue_withPlaceHolderInSendToName_templateIsCalled() throws Exception {
		// Arrange
		Method validSendToMethod = this.getClass().getDeclaredMethod("placeHolderMethod");
		MethodParameter methodParameter = new MethodParameter(validSendToMethod, -1);

		GenericApplicationContext applicationContext = new GenericApplicationContext();
		MockPropertySource propertySource = new MockPropertySource();
		propertySource.setProperty("placeholderQueueName", "myTestQueue");

		applicationContext.getEnvironment().getPropertySources().addLast(propertySource);
		applicationContext.registerBeanDefinition("resolver", BeanDefinitionBuilder
				.genericBeanDefinition(PropertySourcesPlaceholderConfigurer.class).getBeanDefinition());

		applicationContext.refresh();

		SendToHandlerMethodReturnValueHandler sendToHandlerMethodReturnValueHandler;
		sendToHandlerMethodReturnValueHandler = new SendToHandlerMethodReturnValueHandler(this.messageTemplate);
		sendToHandlerMethodReturnValueHandler.setBeanFactory(applicationContext.getAutowireCapableBeanFactory());

		// Act
		sendToHandlerMethodReturnValueHandler.handleReturnValue("placeholder method", methodParameter,
				MessageBuilder.withPayload("Nothing").build());

		// Assert
		verify(this.messageTemplate, times(1)).convertAndSend(eq("myTestQueue"), eq("placeholder method"));
	}

	// @checkstyle:off
	@Test
	void handleReturnValue_withAMessageTemplateAndAValidMethodWithoutDestination_templateIsCalled() throws Exception {
		// @checkstyle:on
		// Arrange
		Method validSendToMethod = this.getClass().getDeclaredMethod("anotherValidSendToMethod");
		MethodParameter methodParameter = new MethodParameter(validSendToMethod, -1);
		SendToHandlerMethodReturnValueHandler sendToHandlerMethodReturnValueHandler;
		sendToHandlerMethodReturnValueHandler = new SendToHandlerMethodReturnValueHandler(this.messageTemplate);

		// Act
		sendToHandlerMethodReturnValueHandler.handleReturnValue("Another Elastic Hello!", methodParameter,
				MessageBuilder.withPayload("Nothing").build());

		// Assert
		verify(this.messageTemplate, times(1)).convertAndSend(eq("Another Elastic Hello!"));
	}

	@SuppressWarnings("SameReturnValue")
	@RuntimeUse
	@SendTo("testQueue")
	private String validSendToMethod() {
		return "Elastic Hello!";
	}

	@SuppressWarnings("SameReturnValue")
	@RuntimeUse
	@SendTo
	private String anotherValidSendToMethod() {
		return "Another Elastic Hello!";
	}

	@SuppressWarnings("SameReturnValue")
	@RuntimeUse
	private String invalidSendToMethod() {
		return "Just Hello!";
	}

	@SuppressWarnings("SameReturnValue")
	@RuntimeUse
	@SendTo("#{environment.queueName}")
	private String expressionMethod() {
		return "expression method";
	}

	@SuppressWarnings("SameReturnValue")
	@RuntimeUse
	@SendTo("${placeholderQueueName}")
	private String placeHolderMethod() {
		return "placeholder method";
	}

}
