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

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.exceptions.base.MockitoException;
import org.mockitoutil.TestBase;

import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNotNull;
import static junit.framework.TestCase.assertTrue;
import static junit.framework.TestCase.fail;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@SuppressWarnings("unused")
public class SpyAnnotationTest extends TestBase {

    @Spy
    final List<String> spiedList = new ArrayList<String>();

    @Spy
    InnerStaticClassWithNoArgConstructor staticTypeWithNoArgConstructor;

    @Spy
    InnerStaticClassWithoutDefinedConstructor staticTypeWithoutDefinedConstructor;

    @Rule
    public final ExpectedException shouldThrow = ExpectedException.none();

    @Test
    public void should_init_spy_by_instance() throws Exception {
        doReturn("foo").when(spiedList).get(10);
        assertEquals("foo", spiedList.get(10));
        assertTrue(spiedList.isEmpty());
    }

    @Test
    public void should_init_spy_and_automatically_create_instance() throws Exception {
        when(staticTypeWithNoArgConstructor.toString()).thenReturn("x");
        when(staticTypeWithoutDefinedConstructor.toString()).thenReturn("y");
        assertEquals("x", staticTypeWithNoArgConstructor.toString());
        assertEquals("y", staticTypeWithoutDefinedConstructor.toString());
    }

    @Test
    public void should_allow_spying_on_interfaces() throws Exception {
        class WithSpy {
            @Spy
            List<String> list;
        }

        WithSpy withSpy = new WithSpy();
        MockitoAnnotations.initMocks(withSpy);
        when(withSpy.list.size()).thenReturn(3);
        assertEquals(3, withSpy.list.size());
    }

    @Test
    public void should_allow_spying_on_interfaces_when_instance_is_concrete() throws Exception {
        class WithSpy {
            @Spy
            List<String> list = new LinkedList<String>();
        }
        WithSpy withSpy = new WithSpy();

        //when
        MockitoAnnotations.initMocks(withSpy);

        //then
        verify(withSpy.list, never()).clear();
    }

    @Test
    public void should_report_when_no_arg_less_constructor() throws Exception {
        class FailingSpy {
            @Spy
            NoValidConstructor noValidConstructor;
        }

        try {
            MockitoAnnotations.initMocks(new FailingSpy());
            fail();
        } catch (MockitoException e) {
            assertThat(e.getMessage()).contains("Please ensure that the type")
                    .contains(NoValidConstructor.class.getSimpleName())
                    .contains("has a no-arg constructor");
        }
    }

    @Test
    public void should_report_when_constructor_is_explosive() throws Exception {
        class FailingSpy {
            @Spy
            ThrowingConstructor throwingConstructor;
        }

        try {
            MockitoAnnotations.initMocks(new FailingSpy());
            fail();
        } catch (MockitoException e) {
            assertThat(e.getMessage()).contains("Unable to create mock instance");
        }
    }

    @Test
    public void should_spy_abstract_class() throws Exception {
        class SpyAbstractClass {
            @Spy
            AbstractList<String> list;

            List<String> asSingletonList(String s) {
                when(list.size()).thenReturn(1);
                when(list.get(0)).thenReturn(s);
                return list;
            }
        }
        SpyAbstractClass withSpy = new SpyAbstractClass();
        MockitoAnnotations.initMocks(withSpy);
        assertEquals(Arrays.asList("a"), withSpy.asSingletonList("a"));
    }

    @Test
    public void should_spy_inner_class() throws Exception {

        class WithMockAndSpy {
            @Spy
            private InnerStrength strength;
            @Mock
            private List<String> list;

            abstract class InnerStrength {
                private final String name;

                InnerStrength() {
                    // Make sure that @Mock fields are always injected before @Spy fields.
                    assertNotNull(list);
                    // Make sure constructor is indeed called.
                    this.name = "inner";
                }

                abstract String strength();

                String fullStrength() {
                    return name + " " + strength();
                }
            }
        }
        WithMockAndSpy outer = new WithMockAndSpy();
        MockitoAnnotations.initMocks(outer);
        when(outer.strength.strength()).thenReturn("strength");
        assertEquals("inner strength", outer.strength.fullStrength());
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void should_reset_spy() throws Exception {
        spiedList.get(10); // see shouldInitSpy
    }

    @Test
    public void should_report_when_enclosing_instance_is_needed() throws Exception {
        class Outer {
            class Inner {
            }
        }
        class WithSpy {
            @Spy
            private Outer.Inner inner;
        }
        try {
            MockitoAnnotations.initMocks(new WithSpy());
            fail();
        } catch (MockitoException e) {
            assertThat(e).hasMessageContaining("@Spy annotation can only initialize inner classes");
        }
    }

    @Test
    public void should_report_private_inner_not_supported() throws Exception {
        try {
            MockitoAnnotations.initMocks(new WithInnerPrivate());
            fail();
        } catch (MockitoException e) {
            // Currently fails at instantiation time, because the mock subclass don't have the
            // 1-arg constructor expected for the outerclass.
            // org.mockito.internal.creation.instance.ConstructorInstantiator.withParams()
            assertThat(e).hasMessageContaining("Unable to initialize @Spy annotated field 'spy_field'")
                    .hasMessageContaining(WithInnerPrivate.InnerPrivate.class.getSimpleName());
        }
    }

    @Test
    public void should_report_private_abstract_inner_not_supported() throws Exception {
        try {
            MockitoAnnotations.initMocks(new WithInnerPrivateAbstract());
            fail();
        } catch (MockitoException e) {
            assertThat(e).hasMessageContaining("@Spy annotation can't initialize private abstract inner classes")
                    .hasMessageContaining(WithInnerPrivateAbstract.class.getSimpleName())
                    .hasMessageContaining(WithInnerPrivateAbstract.InnerPrivateAbstract.class.getSimpleName())
                    .hasMessageContaining("You should augment the visibility of this inner class");
        }
    }

    @Test
    public void should_report_private_static_abstract_inner_not_supported() throws Exception {
        try {
            MockitoAnnotations.initMocks(new WithInnerPrivateStaticAbstract());
            fail();
        } catch (MockitoException e) {
            assertThat(e).hasMessageContaining("@Spy annotation can't initialize private abstract inner classes")
                    .hasMessageContaining(WithInnerPrivateStaticAbstract.class.getSimpleName())
                    .hasMessageContaining(WithInnerPrivateStaticAbstract.InnerPrivateStaticAbstract.class.getSimpleName())
                    .hasMessageContaining("You should augment the visibility of this inner class");
        }
    }

    static class WithInnerPrivateStaticAbstract {
        @Spy
        private InnerPrivateStaticAbstract spy_field;

        private static abstract class InnerPrivateStaticAbstract {
        }
    }
    static class WithInnerPrivateAbstract {
        @Spy
        private InnerPrivateAbstract spy_field;

        public void some_method() {
            new InnerPrivateConcrete();
        }

        private abstract class InnerPrivateAbstract {
        }

        private class InnerPrivateConcrete extends InnerPrivateAbstract {

        }
    }

    static class WithInnerPrivate {
        @Spy
        private InnerPrivate spy_field;

        private class InnerPrivate {
        }

        private class InnerPrivateSub extends InnerPrivate {}
    }

    static class InnerStaticClassWithoutDefinedConstructor {
    }

    static class InnerStaticClassWithNoArgConstructor {
        InnerStaticClassWithNoArgConstructor() {
        }

        InnerStaticClassWithNoArgConstructor(String f) {
        }
    }

    static class NoValidConstructor {
        NoValidConstructor(String f) {
        }
    }

    static class ThrowingConstructor {
        ThrowingConstructor() {
            throw new RuntimeException("boo!");
        }
    }
}