/*
 * Copyright 2002-2016 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.handler.invocation;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;

import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.MethodIntrospector;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.SimpleMessageConverter;
import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.util.ReflectionUtils.MethodFilter;

import static org.junit.Assert.*;

/**
 * Test fixture for
 * {@link org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler}.
 *
 * @author Brian Clozel
 * @author Rossen Stoyanchev
 */
public class MethodMessageHandlerTests {

	private static final String DESTINATION_HEADER = "destination";

	private TestMethodMessageHandler messageHandler;

	private TestController testController;


	@Before
	public void setup() {

		List<String> destinationPrefixes = Arrays.asList("/test");

		this.messageHandler = new TestMethodMessageHandler();
		this.messageHandler.setApplicationContext(new StaticApplicationContext());
		this.messageHandler.setDestinationPrefixes(destinationPrefixes);
		this.messageHandler.afterPropertiesSet();

		this.testController = new TestController();
		this.messageHandler.registerHandler(this.testController);
	}

	@Test(expected = IllegalStateException.class)
	public void duplicateMapping() {
		this.messageHandler.registerHandler(new DuplicateMappingsController());
	}

	@Test
	public void registeredMappings() {

		Map<String, HandlerMethod> handlerMethods = this.messageHandler.getHandlerMethods();

		assertNotNull(handlerMethods);
		assertThat(handlerMethods.keySet(), Matchers.hasSize(3));
	}

	@Test
	public void antPatchMatchWildcard() throws Exception {

		Method method = this.testController.getClass().getMethod("handlerPathMatchWildcard");
		this.messageHandler.registerHandlerMethod(this.testController, method, "/handlerPathMatch*");

		this.messageHandler.handleMessage(toDestination("/test/handlerPathMatchFoo"));

		assertEquals("pathMatchWildcard", this.testController.method);
	}

	@Test
	public void bestMatchWildcard() throws Exception {

		Method method = this.testController.getClass().getMethod("bestMatch");
		this.messageHandler.registerHandlerMethod(this.testController, method, "/bestmatch/{foo}/path");

		method = this.testController.getClass().getMethod("secondBestMatch");
		this.messageHandler.registerHandlerMethod(this.testController, method, "/bestmatch/*/*");

		this.messageHandler.handleMessage(toDestination("/test/bestmatch/bar/path"));

		assertEquals("bestMatch", this.testController.method);
	}

	@Test
	public void argumentResolution() {

		this.messageHandler.handleMessage(toDestination("/test/handlerArgumentResolver"));

		assertEquals("handlerArgumentResolver", this.testController.method);
		assertNotNull(this.testController.arguments.get("message"));
	}

	@Test
	public void exceptionHandled() {

		this.messageHandler.handleMessage(toDestination("/test/handlerThrowsExc"));

		assertEquals("illegalStateException", this.testController.method);
		assertNotNull(this.testController.arguments.get("exception"));
	}

	private Message<?> toDestination(String destination) {
		return MessageBuilder.withPayload(new byte[0]).setHeader(DESTINATION_HEADER, destination).build();
	}


	@SuppressWarnings("unused")
	private static class TestController {

		public String method;

		private Map<String, Object> arguments = new LinkedHashMap<>();

		public void handlerPathMatchWildcard() {
			this.method = "pathMatchWildcard";
		}

		@SuppressWarnings("rawtypes")
		public void handlerArgumentResolver(Message message) {
			this.method = "handlerArgumentResolver";
			this.arguments.put("message", message);
		}

		public void handlerThrowsExc() {
			throw new IllegalStateException();
		}

		public void bestMatch() {
			this.method = "bestMatch";
		}

		public void secondBestMatch() {
			this.method = "secondBestMatch";
		}

		public void illegalStateException(IllegalStateException exception) {
			this.method = "illegalStateException";
			this.arguments.put("exception", exception);
		}

	}

	@SuppressWarnings("unused")
	private static class DuplicateMappingsController {

		public void handlerFoo() { }

		public void handlerFoo(String arg) { }
	}


	private static class TestMethodMessageHandler extends AbstractMethodMessageHandler<String> {

		private PathMatcher pathMatcher = new AntPathMatcher();

		public void registerHandler(Object handler) {
			super.detectHandlerMethods(handler);
		}

		public void registerHandlerMethod(Object handler, Method method, String mapping) {
			super.registerHandlerMethod(handler, method, mapping);
		}

		@Override
		protected List<? extends HandlerMethodArgumentResolver> initArgumentResolvers() {
			List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
			resolvers.add(new MessageMethodArgumentResolver(new SimpleMessageConverter()));
			resolvers.addAll(getCustomArgumentResolvers());
			return resolvers;
		}

		@Override
		protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() {
			List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();
			handlers.addAll(getCustomReturnValueHandlers());
			return handlers;
		}

		@Override
		protected boolean isHandler(Class<?> beanType) {
			return beanType.getName().contains("Controller");
		}

		@Override
		protected String getMappingForMethod(Method method, Class<?> handlerType) {
			String methodName = method.getName();
			if (methodName.startsWith("handler")) {
				return "/" + methodName;
			}
			return null;
		}

		@Override
		protected Set<String> getDirectLookupDestinations(String mapping) {
			Set<String> result = new LinkedHashSet<>();
			if (!this.pathMatcher.isPattern(mapping)) {
				result.add(mapping);
			}
			return result;
		}

		@Override
		protected String getDestination(Message<?> message) {
			return (String) message.getHeaders().get(DESTINATION_HEADER);
		}

		@Override
		protected String getMatchingMapping(String mapping, Message<?> message) {

			String destination = getLookupDestination(getDestination(message));
			if (mapping.equals(destination) || this.pathMatcher.match(mapping, destination)) {
				return mapping;
			}
			return null;
		}

		@Override
		protected Comparator<String> getMappingComparator(final Message<?> message) {
			return new Comparator<String>() {
				@Override
				public int compare(String info1, String info2) {
					DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1);
					DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2);
					return cond1.compareTo(cond2, message);
				}
			};
		}

		@Override
		protected AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class<?> beanType) {
			return new TestExceptionHandlerMethodResolver(beanType);
		}
	}


	private static class TestExceptionHandlerMethodResolver extends AbstractExceptionHandlerMethodResolver {

		public TestExceptionHandlerMethodResolver(Class<?> handlerType) {
			super(initExceptionMappings(handlerType));
		}

		private static Map<Class<? extends Throwable>, Method> initExceptionMappings(Class<?> handlerType) {
			Map<Class<? extends Throwable>, Method> result = new HashMap<>();
			for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) {
				for (Class<? extends Throwable> exception : getExceptionsFromMethodSignature(method)) {
					result.put(exception, method);
				}
			}
			return result;
		}

		public final static MethodFilter EXCEPTION_HANDLER_METHOD_FILTER = new MethodFilter() {
			@Override
			public boolean matches(Method method) {
				return method.getName().contains("Exception");
			}
		};
	}

}
