/*
 * Copyright (c) 2007 Mockito contributors
 * This program is made available under the terms of the MIT License.
 */

package org.mockito.internal.util.reflection;


import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.internal.util.reflection.FieldInitializer.ConstructorArgumentResolver;
import org.mockito.internal.util.reflection.FieldInitializer.ParameterizedConstructorInstantiator;
import org.mockito.runners.MockitoJUnitRunner;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Observer;
import java.util.Set;

import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;


@SuppressWarnings("unchecked")
@RunWith(MockitoJUnitRunner.class)
public class ParameterizedConstructorInstantiatorTest {

    private Set whateverForNow;
    private OneConstructor withOneConstructor;
    private MultipleConstructor withMultipleConstructor;
    private NoArgConstructor withNoArgConstructor;
    private ThrowingConstructor withThrowingConstructor;
    private VarargConstructor withVarargConstructor;

    @After
    public void ensure_instances_to_create_are_null() {
        withMultipleConstructor = null;
        withOneConstructor = null;
        withNoArgConstructor = null;
        withThrowingConstructor = null;
        withVarargConstructor = null;
    }

    @Mock private ConstructorArgumentResolver resolver;

    @Test
    public void should_be_created_with_an_argument_resolver() throws Exception {
        new ParameterizedConstructorInstantiator(this, field("whateverForNow"), resolver);
    }

    @Test
    public void should_fail_if_no_parameterized_constructor_found___excluding_inner_and_others_kind_of_types() throws Exception {
        try {
            new ParameterizedConstructorInstantiator(this, field("withNoArgConstructor"), resolver).instantiate();
            fail();
        } catch (MockitoException me) {
            assertThat(me.getMessage()).contains("no parameterized constructor").contains("withNoArgConstructor").contains("NoArgConstructor");
        }
    }

    @Test
    public void should_instantiate_type_if_resolver_provide_matching_types() throws Exception {
        Observer observer = mock(Observer.class);
        Map map = mock(Map.class);
        given(resolver.resolveTypeInstances(Matchers.<Class<?>[]>anyVararg())).willReturn(new Object[]{ observer, map });

        new ParameterizedConstructorInstantiator(this, field("withMultipleConstructor"), resolver).instantiate();

        assertNotNull(withMultipleConstructor);
        assertNotNull(withMultipleConstructor.observer);
        assertNotNull(withMultipleConstructor.map);
    }

    @Test
    public void should_fail_if_an_argument_instance_type_do_not_match_wanted_type() throws Exception {
        Observer observer = mock(Observer.class);
        Set wrongArg = mock(Set.class);
        given(resolver.resolveTypeInstances(Matchers.<Class<?>[]>anyVararg())).willReturn(new Object[]{ observer, wrongArg });

        try {
            new ParameterizedConstructorInstantiator(this, field("withMultipleConstructor"), resolver).instantiate();
            fail();
        } catch (MockitoException e) {
            assertThat(e.getMessage()).contains("argResolver").contains("incorrect types");
        }
    }

    @Test
    public void should_report_failure_if_constructor_throws_exception() throws Exception {
        given(resolver.resolveTypeInstances(Matchers.<Class<?>[]>anyVararg())).willReturn(new Object[]{ null });

        try {
            new ParameterizedConstructorInstantiator(this, field("withThrowingConstructor"), resolver).instantiate();
            fail();
        } catch (MockitoException e) {
            assertThat(e.getMessage()).contains("constructor").contains("raised an exception");
        }
    }

    @Test
    public void should_instantiate_type_with_vararg_constructor() throws Exception {
        Observer[] vararg = new Observer[] {  };
        given(resolver.resolveTypeInstances(Matchers.<Class<?>[]>anyVararg())).willReturn(new Object[]{ "", vararg});

        new ParameterizedConstructorInstantiator(this, field("withVarargConstructor"), resolver).instantiate();

        assertNotNull(withVarargConstructor);
    }

    private Field field(String fieldName) throws NoSuchFieldException {
        Field field = this.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return field;
    }

    private static class NoArgConstructor {
        NoArgConstructor() { }
    }

    private static class OneConstructor {
        public OneConstructor(Observer observer) { }
    }

    private static class ThrowingConstructor {
        public ThrowingConstructor(Observer observer) throws IOException { throw new IOException(); }
    }

    private static class MultipleConstructor extends OneConstructor {
        Observer observer;
        Map map;

        public MultipleConstructor(Observer observer) { this(observer, null); }
        public MultipleConstructor(Observer observer, Map map) {
            super(observer);
            this.observer = observer;
            this.map = map;
        }
    }

    private static class VarargConstructor {
        VarargConstructor(String whatever, Observer... observers) { }
    }
}
