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

import org.assertj.core.api.Assertions;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.exceptions.misusing.PotentialStubbingProblem;
import org.mockito.exceptions.misusing.UnfinishedVerificationException;
import org.mockito.exceptions.misusing.UnnecessaryStubbingException;
import org.mockito.junit.MockitoJUnit;
import org.mockito.quality.Strictness;
import org.mockitousage.IMethods;
import org.mockitoutil.SafeJUnitRule;

import static org.junit.Assert.assertEquals;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockitoutil.TestBase.filterLineNo;

public class StrictJUnitRuleTest {

    @Rule public SafeJUnitRule rule = new SafeJUnitRule(MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS));

    @Mock IMethods mock;
    @Mock IMethods mock2;

    @Test public void ok_when_no_stubbings() throws Throwable {
        mock.simpleMethod();
        verify(mock).simpleMethod();
    }

    @Test public void ok_when_all_stubbings_used() throws Throwable {
        given(mock.simpleMethod(10)).willReturn("foo");
        mock.simpleMethod(10);
    }

    @Test public void ok_when_used_and_mismatched_argument() throws Throwable {
        given(mock.simpleMethod(10)).willReturn("foo");
        mock.simpleMethod(10);
        mock.simpleMethod(15);
    }

    @Test public void fails_when_unused_stubbings() throws Throwable {
        //expect
        rule.expectFailure(UnnecessaryStubbingException.class);

        //when
        given(mock.simpleMethod(10)).willReturn("foo");
        mock2.simpleMethod(10);
    }

    @Test public void test_failure_trumps_unused_stubbings() throws Throwable {
        //expect
        rule.expectFailure(AssertionError.class, "x");

        //when
        given(mock.simpleMethod(10)).willReturn("foo");
        mock.otherMethod();

        throw new AssertionError("x");
    }

    @Test public void why_do_return_syntax_is_useful() throws Throwable {
        //Trade-off of Mockito strictness documented in test

        //expect
        rule.expectFailure(PotentialStubbingProblem.class);

        //when
        when(mock.simpleMethod(10)).thenReturn("10");
        when(mock.simpleMethod(20)).thenReturn("20");
    }

    @Test public void fails_fast_when_stubbing_invoked_with_different_argument() throws Throwable {
        //expect
        rule.expectFailure(new SafeJUnitRule.FailureAssert() {
            public void doAssert(Throwable t) {
                Assertions.assertThat(t).isInstanceOf(PotentialStubbingProblem.class);
                assertEquals(filterLineNo("\n" +
                                "Strict stubbing argument mismatch. Please check:\n" +
                                " - this invocation of 'simpleMethod' method:\n" +
                                "    mock.simpleMethod(15);\n" +
                                "    -> at org.mockitousage.junitrule.StrictJUnitRuleTest.fails_fast_when_stubbing_invoked_with_different_argument(StrictJUnitRuleTest.java:0)\n" +
                                " - has following stubbing(s) with different arguments:\n" +
                                "    1. mock.simpleMethod(20);\n" +
                                "      -> at org.mockitousage.junitrule.StrictJUnitRuleTest.fails_fast_when_stubbing_invoked_with_different_argument(StrictJUnitRuleTest.java:0)\n" +
                                "    2. mock.simpleMethod(30);\n" +
                                "      -> at org.mockitousage.junitrule.StrictJUnitRuleTest.fails_fast_when_stubbing_invoked_with_different_argument(StrictJUnitRuleTest.java:0)\n" +
                                "Typically, stubbing argument mismatch indicates user mistake when writing tests.\n" +
                                "Mockito fails early so that you can debug potential problem easily.\n" +
                                "However, there are legit scenarios when this exception generates false negative signal:\n" +
                                "  - stubbing the same method multiple times using 'given().will()' or 'when().then()' API\n" +
                                "    Please use 'will().given()' or 'doReturn().when()' API for stubbing.\n" +
                                "  - stubbed method is intentionally invoked with different arguments by code under test\n" +
                                "    Please use default or 'silent' JUnit Rule (equivalent of Strictness.LENIENT).\n" +
                                "For more information see javadoc for PotentialStubbingProblem class."),
                        filterLineNo(t.getMessage()));
            }
        });

        //when stubbings in the test code:
        willReturn("10").given(mock).simpleMethod(10) ;  //used
        willReturn("20").given(mock).simpleMethod(20) ;  //unused
        willReturn("30").given(mock).simpleMethod(30) ;  //unused

        //then
        mock.otherMethod(); //ok, different method
        mock.simpleMethod(10); //ok, stubbed with this argument

        //invocation in the code under test uses different argument and should fail immediately
        //this helps with debugging and is essential for Mockito strictness
        mock.simpleMethod(15);
    }

    @Test public void verify_no_more_interactions_ignores_stubs() throws Throwable {
        //when stubbing in test:
        given(mock.simpleMethod(10)).willReturn("foo");

        //and code under test does:
        mock.simpleMethod(10); //implicitly verifies the stubbing
        mock.otherMethod();

        //and in test we:
        verify(mock).otherMethod();
        verifyNoMoreInteractions(mock);
    }

    @Test public void unused_stubs_with_multiple_mocks() throws Throwable {
        //expect
        rule.expectFailure(new SafeJUnitRule.FailureAssert() {
            public void doAssert(Throwable t) {
                assertEquals(filterLineNo("\n" +
                        "Unnecessary stubbings detected.\n" +
                        "Clean & maintainable test code requires zero unnecessary code.\n" +
                        "Following stubbings are unnecessary (click to navigate to relevant line of code):\n" +
                        "  1. -> at org.mockitousage.junitrule.StrictJUnitRuleTest.unused_stubs_with_multiple_mocks(StrictJUnitRuleTest.java:0)\n" +
                        "  2. -> at org.mockitousage.junitrule.StrictJUnitRuleTest.unused_stubs_with_multiple_mocks(StrictJUnitRuleTest.java:0)\n" +
                        "Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class."), filterLineNo(t.getMessage()));
            }
        });

        //when test has
        given(mock.simpleMethod(10)).willReturn("foo");
        given(mock2.simpleMethod(20)).willReturn("foo");

        given(mock.otherMethod()).willReturn("foo"); //used and should not be reported

        //and code has
        mock.otherMethod();
        mock2.booleanObjectReturningMethod();
    }

    @SuppressWarnings({"MockitoUsage", "CheckReturnValue"})
    @Test public void rule_validates_mockito_usage() throws Throwable {
        //expect
        rule.expectFailure(UnfinishedVerificationException.class);

        //when test contains unfinished verification
        verify(mock);
    }
}
