/*
 * Copyright 2012-2017 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
 *
 *      http://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.boot.test.mock.mockito;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;

import org.hamcrest.Matcher;
import org.mockito.Answers;
import org.mockito.internal.InternalMockHandler;
import org.mockito.internal.matchers.LocalizedMatcher;
import org.mockito.internal.progress.ArgumentMatcherStorage;
import org.mockito.internal.progress.MockingProgress;
import org.mockito.internal.progress.ThreadSafeMockingProgress;
import org.mockito.internal.stubbing.InvocationContainer;
import org.mockito.internal.util.MockUtil;
import org.mockito.internal.verification.MockAwareVerificationMode;
import org.mockito.mock.MockCreationSettings;
import org.mockito.stubbing.Answer;
import org.mockito.verification.VerificationMode;

import org.springframework.beans.BeanUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

/**
 * A facade for Mockito APIs that have changed between Mockito 1 and Mockito 2.
 *
 * @author Andy Wilkinson
 * @author Stephane Nicoll
 * @author Phillip Webb
 */
abstract class MockitoApi {

	private static final MockitoApi api = createApi();

	/**
	 * Return mock settings for the given mock object.
	 * @param mock the mock object
	 * @return the mock creation settings
	 */
	public abstract MockCreationSettings<?> getMockSettings(Object mock);

	/**
	 * Return the mocking progress for the current thread.
	 * @param mock the mock object
	 * @return the current mocking progress
	 */
	public abstract MockingProgress mockingProgress(Object mock);

	/**
	 * Set report matchers to the given storage.
	 * @param storage the storage to use
	 * @param matchers the matchers to set
	 */
	public abstract void reportMatchers(ArgumentMatcherStorage storage,
			List<LocalizedMatcher> matchers);

	/**
	 * Create a new {@link MockAwareVerificationMode} instance.
	 * @param mock the source mock
	 * @param mode the verification mode
	 * @return a new {@link MockAwareVerificationMode} instance
	 */
	public abstract MockAwareVerificationMode createMockAwareVerificationMode(Object mock,
			VerificationMode mode);

	/**
	 * Return the {@link Answer} for a given {@link Answers} value.
	 * @param answer the source answers
	 * @return the answer
	 */
	public abstract Answer<Object> getAnswer(Answers answer);

	/**
	 * Factory to create the appropriate API version.
	 * @return the API version
	 */
	private static MockitoApi createApi() {
		if (ClassUtils.isPresent("org.mockito.ReturnValues", null)) {
			return new Mockito1Api();
		}
		return new Mockito2Api();
	}

	/**
	 * Get the API for the running mockito version.
	 * @return the API
	 */
	public static MockitoApi get() {
		return api;
	}

	/**
	 * {@link MockitoApi} for Mockito 1.0.
	 */
	private static class Mockito1Api extends MockitoApi {

		private final MockUtil mockUtil;

		private final Method getMockSettingsMethod;

		private final Method getMockHandlerMethod;

		private Method reportMatcherMethod;

		private Constructor<MockAwareVerificationMode> mockAwareVerificationModeConstructor;

		Mockito1Api() {
			this.mockUtil = BeanUtils.instantiateClass(MockUtil.class);
			this.getMockSettingsMethod = ReflectionUtils.findMethod(MockUtil.class,
					"getMockSettings", Object.class);
			this.getMockHandlerMethod = ReflectionUtils.findMethod(MockUtil.class,
					"getMockHandler", Object.class);
			this.reportMatcherMethod = ReflectionUtils.findMethod(
					ArgumentMatcherStorage.class, "reportMatcher", Matcher.class);
			this.mockAwareVerificationModeConstructor = ClassUtils
					.getConstructorIfAvailable(MockAwareVerificationMode.class,
							Object.class, VerificationMode.class);
		}

		@Override
		public MockCreationSettings<?> getMockSettings(Object mock) {
			return (MockCreationSettings<?>) ReflectionUtils
					.invokeMethod(this.getMockSettingsMethod, this.mockUtil, mock);
		}

		@Override
		public MockingProgress mockingProgress(Object mock) {
			InternalMockHandler<?> handler = (InternalMockHandler<?>) ReflectionUtils
					.invokeMethod(this.getMockHandlerMethod, this.mockUtil, mock);
			InvocationContainer container = handler.getInvocationContainer();
			Field field = ReflectionUtils.findField(container.getClass(),
					"mockingProgress");
			ReflectionUtils.makeAccessible(field);
			return (MockingProgress) ReflectionUtils.getField(field, container);
		}

		@Override
		public void reportMatchers(ArgumentMatcherStorage storage,
				List<LocalizedMatcher> matchers) {
			for (LocalizedMatcher matcher : matchers) {
				ReflectionUtils.invokeMethod(this.reportMatcherMethod, storage, matcher);
			}
		}

		@Override
		public MockAwareVerificationMode createMockAwareVerificationMode(Object mock,
				VerificationMode mode) {
			return BeanUtils.instantiateClass(this.mockAwareVerificationModeConstructor,
					mock, mode);
		}

		@Override
		@SuppressWarnings("deprecation")
		public Answer<Object> getAnswer(Answers answer) {
			return answer.get();
		}

	}

	/**
	 * {@link MockitoApi} for Mockito 2.0.
	 */
	private static class Mockito2Api extends MockitoApi {

		@Override
		public MockCreationSettings<?> getMockSettings(Object mock) {
			return MockUtil.getMockSettings(mock);
		}

		@Override
		public MockingProgress mockingProgress(Object mock) {
			return ThreadSafeMockingProgress.mockingProgress();
		}

		@Override
		public void reportMatchers(ArgumentMatcherStorage storage,
				List<LocalizedMatcher> matchers) {
			for (LocalizedMatcher matcher : matchers) {
				storage.reportMatcher(matcher.getMatcher());
			}
		}

		@Override
		public MockAwareVerificationMode createMockAwareVerificationMode(Object mock,
				VerificationMode mode) {
			try {
				return new MockAwareVerificationMode(mock, mode, Collections.emptySet());
			}
			catch (NoSuchMethodError ex) {
				// Earlier versions of 2.x did not have the collection parameter
				Constructor<MockAwareVerificationMode> constructor = ClassUtils
						.getConstructorIfAvailable(MockAwareVerificationMode.class,
								Object.class, VerificationMode.class);
				if (constructor == null) {
					throw ex;
				}
				return BeanUtils.instantiateClass(constructor, mock, mode);
			}
		}

		@Override
		public Answer<Object> getAnswer(Answers answer) {
			return answer;
		}

	}

}
