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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import java.io.IOException;

import org.junit.After;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.exceptions.base.MockitoException;
import org.mockito.exceptions.verification.NoInteractionsWanted;
import org.mockito.stubbing.Answer;
import org.mockitousage.IMethods;
import org.mockitousage.MethodsImpl;
import org.mockitoutil.TestBase;

@SuppressWarnings("serial")
public class StubbingUsingDoReturnTest extends TestBase {

    @Mock private IMethods mock;

    @After
    public void reset_state() {
        super.resetState();
    }

    @Test
    public void should_stub() {
        doReturn("foo").when(mock).simpleMethod();
        doReturn("bar").when(mock).simpleMethod();

        assertThat(mock.simpleMethod()).isEqualTo("bar");
    }

    @Test
    public void should_stub_with_args() {
        doReturn("foo").when(mock).simpleMethod("foo");
        doReturn("bar").when(mock).simpleMethod(eq("one"), anyInt());

        assertThat(mock.simpleMethod("foo")).isEqualTo("foo");
        assertThat(mock.simpleMethod("one", 234)).isEqualTo("bar");
        assertThat(mock.simpleMethod("xxx", 234)).isNull();
    }

    class FooRuntimeException extends RuntimeException {}

    @Test
    public void should_stub_with_throwable() {
        doThrow(new FooRuntimeException()).when(mock).voidMethod();
        try {
            mock.voidMethod();
            fail();
        } catch (FooRuntimeException e) {
        }
    }

    @Test
    public void should_allow_setting_valid_checked_exception() throws Exception {
        doThrow(new IOException()).when(mock).throwsIOException(0);

        try {
            mock.throwsIOException(0);
            fail();
        } catch (IOException e) {
        }
    }

    class FooCheckedException extends Exception {}

    @Test
    public void should_detect_invalid_checked_exception() {
        try {
            doThrow(new FooCheckedException()).when(mock).throwsIOException(0);
            fail();
        } catch (Exception e) {
            assertThat(e).hasMessageContaining("Checked exception is invalid");
        }
    }

    @Test
    public void should_scream_when_return_set_for_void() {
        try {
            doReturn("foo").when(mock).voidMethod();
            fail();
        } catch (MockitoException e) {
            assertThat(e).hasMessageContaining("void method").hasMessageContaining("cannot");
        }
    }

    @Test
    public void should_scream_when_not_a_mock_passed() {
        try {
            doReturn("foo").when("foo").toString();
            fail();
        } catch (Exception e) {
            assertThat(e).hasMessageContaining("Argument passed to when() is not a mock");
        }
    }

    @Test
    public void should_scream_when_null_passed() {
        try {
            doReturn("foo").when((Object) null).toString();
            fail();
        } catch (Exception e) {
            assertThat(e).hasMessageContaining("Argument passed to when() is null");
        }
    }

    @Test
    public void should_allow_chained_stubbing() {
        doReturn("foo").doThrow(new RuntimeException()).doReturn("bar").when(mock).simpleMethod();

        assertThat(mock.simpleMethod()).isEqualTo("foo");
        try {
            mock.simpleMethod();
            fail();
        } catch (RuntimeException expected) {
        }

        assertThat(mock.simpleMethod()).isEqualTo("bar");
        assertThat(mock.simpleMethod()).isEqualTo("bar");
    }

    @Test
    public void should_allow_consecutive_return_values() {
        doReturn("foo", "bar")
                .doThrow(new RuntimeException())
                .doReturn(430L, new byte[0], "qix")
                .when(mock)
                .objectReturningMethodNoArgs();

        assertThat(mock.objectReturningMethodNoArgs()).isEqualTo("foo");
        assertThat(mock.objectReturningMethodNoArgs()).isEqualTo("bar");
        try {
            mock.objectReturningMethodNoArgs();
            fail("exception not raised");
        } catch (RuntimeException expected) {
        }

        assertThat(mock.objectReturningMethodNoArgs()).isEqualTo(430L);
        assertThat(mock.objectReturningMethodNoArgs()).isEqualTo(new byte[0]);
        assertThat(mock.objectReturningMethodNoArgs()).isEqualTo("qix");
        assertThat(mock.objectReturningMethodNoArgs()).isEqualTo("qix");
    }

    @Test
    public void should_allow_do_call_real_method_in_chained_stubbing() {
        MethodsImpl methods = mock(MethodsImpl.class);
        doReturn("A").doCallRealMethod().when(methods).simpleMethod();

        assertThat(methods.simpleMethod()).isEqualTo("A");
        assertThat(methods.simpleMethod()).isNull();
    }

    @Test
    public void should_allow_chained_stubbing_with_exception_class() {
        doReturn("whatever").doThrow(IllegalArgumentException.class).when(mock).simpleMethod();

        assertThat(mock.simpleMethod()).isEqualTo("whatever");
        assertThatThrownBy(
                        () -> {
                            mock.simpleMethod();
                        })
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    public void should_allow_chained_stubbing_on_void_methods() {
        doNothing().doNothing().doThrow(new RuntimeException()).when(mock).voidMethod();

        mock.voidMethod();
        mock.voidMethod();
        try {
            mock.voidMethod();
            fail();
        } catch (RuntimeException e) {
        }
        try {
            mock.voidMethod();
            fail();
        } catch (RuntimeException e) {
        }
    }

    @Test
    public void should_stub_with_generic_answer() {
        doAnswer((Answer<Object>) invocation -> "foo").when(mock).simpleMethod();

        assertThat(mock.simpleMethod()).isEqualTo("foo");
    }

    @Test
    public void should_not_allow_do_nothing_on_non_voids() {
        try {
            doNothing().when(mock).simpleMethod();
            fail();
        } catch (MockitoException e) {
            assertThat(e).hasMessageContaining("Only void methods can doNothing()");
        }
    }

    @Test
    public void should_stubbing_be_treated_as_interaction() {
        doReturn("foo").when(mock).simpleMethod();
        mock.simpleMethod();
        try {
            verifyNoMoreInteractions(mock);
            fail();
        } catch (NoInteractionsWanted e) {
        }
    }

    @Test
    public void should_verify_stubbed_call() {
        doReturn("foo").when(mock).simpleMethod();
        mock.simpleMethod();
        mock.simpleMethod();

        verify(mock, times(2)).simpleMethod();
        verifyNoMoreInteractions(mock);
    }

    @Test
    public void should_allow_stubbing_to_string() {
        doReturn("test").when(mock).toString();
        assertThat(mock.toString()).isEqualTo("test");
    }

    @Test
    public void should_detect_invalid_return_type() {
        try {
            doReturn("foo").when(mock).booleanObjectReturningMethod();
            fail();
        } catch (Exception e) {
            assertThat(e)
                    .hasMessageContaining(
                            "String cannot be returned by booleanObjectReturningMethod()"
                                    + "\n"
                                    + "booleanObjectReturningMethod() should return Boolean");
        }
    }

    @Test
    public void should_detect_when_null_assigned_to_boolean() {
        try {
            doReturn(null).when(mock).intReturningMethod();
            fail();
        } catch (Exception e) {
            assertThat(e).hasMessageContaining("null cannot be returned by intReturningMethod");
        }
    }

    @Test
    public void should_allow_stubbing_when_types_match_signature() {
        doReturn("foo").when(mock).objectReturningMethodNoArgs();
        doReturn("foo").when(mock).simpleMethod();
        doReturn(1).when(mock).intReturningMethod();
        doReturn(2).when(mock).intReturningMethod();
    }
}
