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

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import org.assertj.core.api.ThrowableAssert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoSession;
import org.mockito.exceptions.misusing.PotentialStubbingProblem;
import org.mockito.exceptions.misusing.UnnecessaryStubbingException;
import org.mockito.exceptions.verification.NoInteractionsWanted;
import org.mockito.quality.Strictness;
import org.mockitousage.IMethods;
import org.mockitoutil.TestBase;

public class StrictnessPerStubbingTest {

    MockitoSession mockito;
    @Mock IMethods mock;

    @Before
    public void before() {
        mockito =
                Mockito.mockitoSession()
                        .initMocks(this)
                        .strictness(Strictness.STRICT_STUBS)
                        .startMocking();
    }

    @Test
    public void potential_stubbing_problem() {
        // when
        when(mock.simpleMethod("1")).thenReturn("1");
        lenient().when(mock.differentMethod("2")).thenReturn("2");

        // then on lenient stubbing, we can call it with different argument:
        mock.differentMethod("200");

        // but on strict stubbing, we cannot:
        assertThatThrownBy(
                        new ThrowableAssert.ThrowingCallable() {
                            @Override
                            public void call() throws Throwable {
                                ProductionCode.simpleMethod(mock, "100");
                            }
                        })
                .isInstanceOf(PotentialStubbingProblem.class);
    }

    @Test
    public void doReturn_syntax() {
        // when
        lenient().doReturn("2").doReturn("3").when(mock).simpleMethod(1);

        // then on lenient stubbing, we can call it with different argument:
        mock.simpleMethod(200);

        // and stubbing works, too:
        assertEquals("2", mock.simpleMethod(1));
        assertEquals("3", mock.simpleMethod(1));
    }

    @Test
    public void doReturn_varargs_syntax() {
        // when
        lenient().doReturn("2", "3").when(mock).simpleMethod(1);

        // then on lenient stubbing, we can call it with different argument with no exception:
        mock.simpleMethod(200);

        // and stubbing works, too:
        assertEquals("2", mock.simpleMethod(1));
        assertEquals("3", mock.simpleMethod(1));
    }

    @Test
    public void doThrow_syntax() {
        // when
        lenient()
                .doThrow(IllegalArgumentException.class)
                .doThrow(IllegalStateException.class)
                .when(mock)
                .simpleMethod(1);

        // then on lenient stubbing, we can call it with different argument with no exception:
        mock.simpleMethod(200);

        // and stubbing works, too:
        assertThatThrownBy(
                        new ThrowableAssert.ThrowingCallable() {
                            public void call() throws Throwable {
                                mock.simpleMethod(1);
                            }
                        })
                .isInstanceOf(IllegalArgumentException.class);

        // testing consecutive call:
        assertThatThrownBy(
                        new ThrowableAssert.ThrowingCallable() {
                            public void call() throws Throwable {
                                mock.simpleMethod(1);
                            }
                        })
                .isInstanceOf(IllegalStateException.class);
    }

    @Test
    public void doThrow_vararg_syntax() {
        // when
        lenient()
                .doThrow(IllegalArgumentException.class, IllegalStateException.class)
                .when(mock)
                .simpleMethod(1);

        // then on lenient stubbing, we can call it with different argument with no exception:
        mock.simpleMethod(200);

        // and stubbing works, too:
        assertThatThrownBy(
                        new ThrowableAssert.ThrowingCallable() {
                            public void call() throws Throwable {
                                mock.simpleMethod(1);
                            }
                        })
                .isInstanceOf(IllegalArgumentException.class);

        // testing consecutive call:
        assertThatThrownBy(
                        new ThrowableAssert.ThrowingCallable() {
                            public void call() throws Throwable {
                                mock.simpleMethod(1);
                            }
                        })
                .isInstanceOf(IllegalStateException.class);
    }

    @Test
    public void doThrow_instance_vararg_syntax() {
        // when
        lenient()
                .doThrow(new IllegalArgumentException(), new IllegalStateException())
                .when(mock)
                .simpleMethod(1);

        // then on lenient stubbing, we can call it with different argument with no exception:
        mock.simpleMethod(200);

        // and stubbing works, too:
        assertThatThrownBy(
                        new ThrowableAssert.ThrowingCallable() {
                            public void call() throws Throwable {
                                mock.simpleMethod(1);
                            }
                        })
                .isInstanceOf(IllegalArgumentException.class);

        // testing consecutive call:
        assertThatThrownBy(
                        new ThrowableAssert.ThrowingCallable() {
                            public void call() throws Throwable {
                                mock.simpleMethod(1);
                            }
                        })
                .isInstanceOf(IllegalStateException.class);
    }

    static class Counter {
        int increment(int x) {
            return x + 1;
        }

        void scream(String message) {
            throw new RuntimeException(message);
        }
    }

    @Test
    public void doCallRealMethod_syntax() {
        // when
        Counter mock = mock(Counter.class);
        lenient().doCallRealMethod().when(mock).increment(1);

        // then no exception and default return value if we call it with different arg:
        assertEquals(0, mock.increment(0));

        // and real method is called when using correct arg:
        assertEquals(2, mock.increment(1));
    }

    @Test
    public void doNothing_syntax() {
        // when
        final Counter spy = spy(Counter.class);
        lenient().doNothing().when(spy).scream("1");

        // then no stubbing exception and real method is called if we call stubbed method with
        // different arg:
        assertThatThrownBy(
                        new ThrowableAssert.ThrowingCallable() {
                            @Override
                            public void call() throws Throwable {
                                spy.scream("2");
                            }
                        })
                .hasMessage("2");

        // and we do nothing when stubbing called with correct arg:
        spy.scream("1");
    }

    @Test
    public void doAnswer_syntax() {
        // when
        lenient().doAnswer(AdditionalAnswers.returnsFirstArg()).when(mock).simpleMethod("1");

        // then on lenient stubbing, we can call it with different argument:
        mock.simpleMethod("200");

        // and stubbing works, too:
        assertEquals("1", mock.simpleMethod("1"));
    }

    @Test
    public void unnecessary_stubbing() {
        // when
        when(mock.simpleMethod("1")).thenReturn("1");
        lenient().when(mock.differentMethod("2")).thenReturn("2");

        // then unnecessary stubbing flags method only on the strict stubbing:
        assertThatThrownBy(
                        new ThrowableAssert.ThrowingCallable() {
                            @Override
                            public void call() throws Throwable {
                                mockito.finishMocking();
                            }
                        })
                .isInstanceOf(UnnecessaryStubbingException.class)
                .hasMessageContaining("1. -> ")
                // good enough to prove that we're flagging just one unnecessary stubbing:
                // TODO 792: this assertion is duplicated with StrictnessPerMockTest
                .isNot(TestBase.hasMessageContaining("2. ->"));
    }

    @Test
    public void unnecessary_stubbing_with_doReturn() {
        // when
        lenient().doReturn("2").when(mock).differentMethod("2");

        // then no exception is thrown:
        mockito.finishMocking();
    }

    @Test
    public void verify_no_more_invocations() {
        // when
        when(mock.simpleMethod("1")).thenReturn("1");
        lenient().when(mock.differentMethod("2")).thenReturn("2");

        // and:
        mock.simpleMethod("1");
        mock.differentMethod("200"); // <- different arg

        // then 'verifyNoMoreInteractions' flags the lenient stubbing (called with different arg)
        // and reports it with [?] in the exception message
        assertThatThrownBy(
                        new ThrowableAssert.ThrowingCallable() {
                            @Override
                            public void call() throws Throwable {
                                verifyNoMoreInteractions(mock);
                            }
                        })
                .isInstanceOf(NoInteractionsWanted.class)
                .hasMessageContaining("1. ->")
                .hasMessageContaining("2. [?]->");
        // TODO 792: assertion duplicated with StrictnessPerMockTest
        // and we should use assertions based on content of the exception rather than the string
    }

    @After
    public void after() {
        mockito.finishMocking();
    }
}
