package com.atlassian.bitbucket.mockito;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.mockito.internal.stubbing.defaultanswers.ReturnsEmptyValues;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;

import static org.mockito.Mockito.mock;

/**
 * An {@link Answer} that will, by default, mock all non-primitive, non-array objects returned from call chains
 * originating from the base mocked object.
 * <p>
 * Sample usage to mock the sequence of calls {@code someMock.someMethod().someSecondMethod().someThirdMethod()... }:
 * <p>
 * {@code when(someMock.someMethod()).then(new MockCallChainAnswer());}
 * <p>
 * Mocks are cached and are keyed by the class that was mocked so that should a sequence of call chains return
 * a given type more than once, the same mock will be returned for each invocation.
 * <p>
 * Methods in the call chain that return arrays or primitive objects will return default values for these (e.g. null
 * for arrays, false for boolean etc).
 * <p>
 * Mocks generated during the call chain are accessible by {@link #mockFor(Class)}} for use in verify expressions and
 * for changing the default behaviour (to return mocked objects or default values as described above).
 * <p>
 * To override the behaviour of a mock, retrieve it from the answer chain and specify its behaviour. E.g.:
 * <p>
 * {@code when(answerChain.mockFor(SomeClass.class).someMethod()).thenThrow(new SomeException()); }
 */
@NotThreadSafe
public class MockCallChainAnswer implements Answer<Object> {
    private final Answer<?> defaultAnswers = new ReturnsEmptyValues();
    private final LoadingCache<Class<?>, Answer<?>> answerCache;
    private final LoadingCache<Class<?>, Object> mockCache;

    @SuppressWarnings("unchecked")
    public MockCallChainAnswer() {
        answerCache = CacheBuilder.newBuilder().build(new CacheLoader() {

            @Override
            public Object load(@Nonnull Object key) throws Exception {
                return new MockCallChainAnswer(answerCache, mockCache);
            }
        });

        mockCache = CacheBuilder.newBuilder().build(new CacheLoader() {

            @Override
            public Object load(@Nonnull Object clazz) throws Exception {
                return mock((Class<?>) clazz, answerCache.getUnchecked((Class<?>) clazz));
            }
        });
    }

    private MockCallChainAnswer(LoadingCache<Class<?>, Answer<?>> answerCache, LoadingCache<Class<?>, Object> mockCache) {
        this.answerCache = answerCache;
        this.mockCache = mockCache;
    }

    @Override
    public Object answer(InvocationOnMock invocation) throws Throwable {
        Class<?> returnType = invocation.getMethod().getReturnType();
        Object defaultAnswer = defaultAnswers.answer(invocation);
        if (defaultAnswer != null) {
            return defaultAnswer;
        }
        return mockCache.getUnchecked(returnType);
    }

    @SuppressWarnings("unchecked")
    public <T> T mockFor(Class<T> mockClass) {
        return (T) mockCache.getUnchecked(mockClass);
    }
}
