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

import org.fest.assertions.Assertions;
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 java.util.*;

import static org.mockito.Mockito.*;

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

    @Spy final List spiedList = new ArrayList();

    @Spy NestedClassWithNoArgConstructor staticTypeWithNoArgConstructor;

    @Spy
    NestedClassWithoutDefinedConstructor 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_prevent_spying_on_interfaces() throws Exception {
		class WithSpy {
			@Spy List<String> list;
		}

		WithSpy withSpy = new WithSpy();
        try {
            MockitoAnnotations.initMocks(withSpy);
            fail();
        } catch (MockitoException e) {
            Assertions.assertThat(e.getMessage()).contains("is an interface and it cannot be spied on");
        }
    }

    @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) {
            Assertions.assertThat(e.getMessage()).contains("0-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) {
            Assertions.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_encosing_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) {
            assertContains("@Spy annotation can only initialize inner classes", e.getMessage());
        }
	}

    static class NestedClassWithoutDefinedConstructor { }

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

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

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