package com.episode6.hackit.mockspresso.internal;

import com.episode6.hackit.mockspresso.DefaultTestRunner;
import com.episode6.hackit.mockspresso.api.DependencyProvider;
import com.episode6.hackit.mockspresso.api.InjectionConfig;
import com.episode6.hackit.mockspresso.reflect.DependencyKey;
import com.episode6.hackit.mockspresso.reflect.NamedAnnotationLiteral;
import com.episode6.hackit.mockspresso.reflect.TypeToken;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.List;

import static org.fest.assertions.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

/**
 * Tests {@link RealObjectMaker}
 */
@RunWith(DefaultTestRunner.class)
public class RealObjectMakerTest {

  static final DependencyKey<Runnable> runnableKey = new DependencyKey<>(TypeToken.of(Runnable.class), null);
  static final DependencyKey<Provider<Runnable>> runnableProviderKey = new DependencyKey<>(
      new TypeToken<Provider<Runnable>>() {},
      new NamedAnnotationLiteral("testprovider"));

  @Mock InjectionConfig.ConstructorSelector mConstructorSelector;
  @Mock DependencyProvider mDependencyProvider;

  @Mock Runnable mRunnableMock;
  @Mock Provider<Runnable> mRunnableProviderMock;

  private RealObjectMaker mRealObjectMaker;

  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
    // testing with first constructor available
    when(mConstructorSelector.chooseConstructor(any(TypeToken.class)))
        .thenAnswer(new Answer<Constructor>() {
          @Override
          public Constructor answer(InvocationOnMock invocation) throws Throwable {
            TypeToken typeToken = invocation.getArgument(0);
            return typeToken.getRawType().getDeclaredConstructors()[0];
          }
        });
  }

  @Test
  public void testWithConstructorOnly() {
    prep();

    when(mDependencyProvider.get(runnableKey)).thenReturn(mRunnableMock);
    when(mDependencyProvider.get(runnableProviderKey)).thenReturn(mRunnableProviderMock);

    TestClassWithConstructorOnly testObject = mRealObjectMaker.createObject(
        mDependencyProvider,
        TypeToken.of(TestClassWithConstructorOnly.class));

    verify(mDependencyProvider).get(runnableKey);
    verify(mDependencyProvider).get(runnableProviderKey);

    assertThat(testObject).isNotNull();
    assertThat(mockingDetails(testObject).isMock()).isFalse();
    assertThat(testObject.mRunnable).isEqualTo(mRunnableMock);
    assertThat(testObject.mRunnableProvider).isEqualTo(mRunnableProviderMock);
  }

  @Test
  public void testWithInjectOnly() {
    prep(Inject.class);

    when(mDependencyProvider.get(runnableKey)).thenReturn(mRunnableMock);
    when(mDependencyProvider.get(runnableProviderKey)).thenReturn(mRunnableProviderMock);

    TestClassWithInjectParams testObject = mRealObjectMaker.createObject(
        mDependencyProvider,
        TypeToken.of(TestClassWithInjectParams.class));

    verify(mDependencyProvider).get(runnableKey);
    verify(mDependencyProvider).get(runnableProviderKey);

    assertThat(testObject).isNotNull();
    assertThat(mockingDetails(testObject).isMock()).isFalse();
    assertThat(testObject.mRunnable).isEqualTo(mRunnableMock);
    assertThat(testObject.mRunnableProvider).isEqualTo(mRunnableProviderMock);
  }

  @Test
  public void testWithConstructorAndInjectMixed() {
    prep(Inject.class);

    when(mDependencyProvider.get(runnableKey)).thenReturn(mRunnableMock);
    when(mDependencyProvider.get(runnableProviderKey)).thenReturn(mRunnableProviderMock);

    TestConstructorAndInject testObject = mRealObjectMaker.createObject(
        mDependencyProvider,
        TypeToken.of(TestConstructorAndInject.class));
    
    verify(mDependencyProvider).get(runnableKey);
    verify(mDependencyProvider).get(runnableProviderKey);

    assertThat(testObject).isNotNull();
    assertThat(mockingDetails(testObject).isMock()).isFalse();
    assertThat(testObject.mRunnable).isEqualTo(mRunnableMock);
    assertThat(testObject.mRunnableProvider).isEqualTo(mRunnableProviderMock);
  }

  @SafeVarargs
  private final void prep(Class<? extends Annotation>... annotations) {
    List<Class<? extends Annotation>> annotationList = Arrays.asList(annotations);
    mRealObjectMaker = new RealObjectMaker(mConstructorSelector, annotationList);
  }

  public static class TestClassWithConstructorOnly {
    final Runnable mRunnable;
    final Provider<Runnable> mRunnableProvider;

    public TestClassWithConstructorOnly(
        Runnable runnable,
        @Named("testprovider") Provider<Runnable> runnableProvider) {
      mRunnable = runnable;
      mRunnableProvider = runnableProvider;
    }
  }

  public static class TestClassWithInjectParams {
    @Inject Runnable mRunnable;
    @Inject @Named("testprovider") Provider<Runnable> mRunnableProvider;
  }

  public static class TestConstructorAndInject {
    final Runnable mRunnable;
    @Inject @Named("testprovider") Provider<Runnable> mRunnableProvider;

    public TestConstructorAndInject(Runnable runnable) {
      mRunnable = runnable;
    }
  }
}

@@ -727,24 +727,55 @@
 t.Provider;%0A
+import javax.inject.Singleton;%0A
 import java.
@@ -4111,20 +4111,820 @@
 lass));%0A
+%0A
     
+verify(mDependencyProvider).get(runnableKey);%0A    verify(mDependencyProvider).get(runnableProviderKey);%0A%0A    assertThat(testObject).isNotNull();%0A    assertThat(mockingDetails(testObject).isMock()).isFalse();%0A    assertThat(testObject.mRunnable).isEqualTo(mRunnableMock);%0A    assertThat(testObject.mRunnableProvider).isEqualTo(mRunnableProviderMock);%0A  %7D%0A%0A%0A  @Test%0A  public void testWithWeirdInjectAnnotations() %7B%0A    prep(Inject.class, Singleton.class);%0A%0A    when(mDependencyProvider.get(runnableKey)).thenReturn(mRunnableMock);%0A    when(mDependencyProvider.get(runnableProviderKey)).thenReturn(mRunnableProviderMock);%0A%0A    TestClassWithWeirdInjectAnnotations testObject = mRealObjectMaker.createObject(%0A        mDependencyProvider,%0A        TypeToken.of(TestClassWithWeirdInjectAnnotations.class));%0A
 %0A    ver
@@ -6292,11 +6292,184 @@
    %7D%0A  %7D
+%0A%0A  public static class TestClassWithWeirdInjectAnnotations %7B%0A    @Inject Runnable mRunnable;%0A    @Singleton @Named(%22testprovider%22) Provider%3CRunnable%3E mRunnableProvider;%0A  %7D
 %0A%7D%0A

add a test for when multiple annotations are used