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

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.Mockito.*;

import java.sql.Date;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.exceptions.verification.NeverWantedButInvoked;
import org.mockito.exceptions.verification.NoInteractionsWanted;
import org.mockito.exceptions.verification.WantedButNotInvoked;
import org.mockito.exceptions.verification.opentest4j.ArgumentsAreDifferent;
import org.mockitousage.IMethods;
import org.mockitoutil.TestBase;

public class DescriptiveMessagesWhenVerificationFailsTest extends TestBase {

    private IMethods mock;

    @Before
    public void setup() {
        mock = Mockito.mock(IMethods.class, "iMethods");
    }

    @Test
    public void should_print_method_name() {
        try {
            verify(mock).simpleMethod();
            fail();
        } catch (WantedButNotInvoked e) {
            String expectedMessage =
                    "\n"
                            + "Wanted but not invoked:"
                            + "\n"
                            + "iMethods.simpleMethod();"
                            + "\n"
                            + "-> at";
            assertThat(e).hasMessageContaining(expectedMessage);
        }
    }

    private class Foo {
        public String toString() {
            return "foo";
        }
    }

    @Test
    public void should_print_method_name_and_arguments() {
        try {
            verify(mock).threeArgumentMethod(12, new Foo(), "xx");
            fail();
        } catch (WantedButNotInvoked e) {
            assertThat(e).hasMessageContaining("iMethods.threeArgumentMethod(12, foo, \"xx\")");
        }
    }

    @Test
    public void should_print_actual_and_wanted_in_line() {
        mock.varargs(1, 2);

        try {
            verify(mock).varargs(1, 1000);
            fail();
        } catch (ArgumentsAreDifferent e) {
            String wanted =
                    "\n"
                            + "Argument(s) are different! Wanted:"
                            + "\n"
                            + "iMethods.varargs(1, 1000);";

            assertThat(e).hasMessageContaining(wanted);

            String actual =
                    "\n"
                            + "Actual invocations have different arguments at position [1]:"
                            + "\n"
                            + "iMethods.varargs(1, 2);";

            assertThat(e).hasMessageContaining(actual);
        }
    }

    @Test
    public void should_print_actual_and_wanted_in_multiple_lines() {
        mock.varargs("this is very long string", "this is another very long string");

        try {
            verify(mock).varargs("x", "y", "z");
            fail();
        } catch (ArgumentsAreDifferent e) {
            String wanted =
                    "\n"
                            + "Argument(s) are different! Wanted:"
                            + "\n"
                            + "iMethods.varargs("
                            + "\n"
                            + "    \"x\","
                            + "\n"
                            + "    \"y\","
                            + "\n"
                            + "    \"z\""
                            + "\n"
                            + ");";

            assertThat(e).hasMessageContaining(wanted);

            String actual =
                    "\n"
                            + "Actual invocations have different arguments:"
                            + "\n"
                            + "iMethods.varargs("
                            + "\n"
                            + "    \"this is very long string\","
                            + "\n"
                            + "    \"this is another very long string\""
                            + "\n"
                            + ");";

            assertThat(e).hasMessageContaining(actual);
        }
    }

    @Test
    public void
            should_print_actual_and_wanted_when_actual_method_name_and_wanted_method_name_are_the_same() {
        mock.simpleMethod();

        try {
            verify(mock).simpleMethod(10);
            fail();
        } catch (ArgumentsAreDifferent e) {
            assertThat(e)
                    .hasMessageContaining("simpleMethod(10)")
                    .hasMessageContaining("simpleMethod()");
        }
    }

    @Test
    public void should_print_actual_and_unverified_wanted_when_the_difference_is_about_arguments() {
        mock.twoArgumentMethod(1, 1);
        mock.twoArgumentMethod(2, 2);

        verify(mock).twoArgumentMethod(1, 1);
        try {
            verify(mock).twoArgumentMethod(2, 1000);
            fail();
        } catch (ArgumentsAreDifferent e) {
            assertThat(e).hasMessageContaining("(2, 1000)").hasMessageContaining("(2, 2)");
        }
    }

    @Test
    public void should_print_first_unexpected_invocation() {
        mock.oneArg(true);
        mock.oneArg(false);
        mock.threeArgumentMethod(1, "2", "3");

        verify(mock).oneArg(true);
        try {
            verifyNoMoreInteractions(mock);
            fail();
        } catch (NoInteractionsWanted e) {
            String expectedMessage = "\n" + "No interactions wanted here:" + "\n" + "-> at";
            assertThat(e).hasMessageContaining(expectedMessage);

            String expectedCause =
                    "\n" + "But found this interaction on mock '" + mock + "':" + "\n" + "-> at";
            assertThat(e).hasMessageContaining(expectedCause);
        }
    }

    @Test
    public void should_print_first_unexpected_invocation_when_verifying_no_interactions() {
        mock.twoArgumentMethod(1, 2);
        mock.threeArgumentMethod(1, "2", "3");

        try {
            verifyNoInteractions(mock);
            fail();
        } catch (NoInteractionsWanted e) {
            String expected = "\n" + "No interactions wanted here:" + "\n" + "-> at";

            assertThat(e).hasMessageContaining(expected);

            String expectedCause =
                    "\n" + "But found these interactions on mock '" + mock + "':" + "\n" + "-> at";

            assertThat(e).hasMessageContaining(expectedCause);
        }
    }

    @Test
    public void should_print_method_name_when_verifying_at_least_once() throws Exception {
        try {
            verify(mock, atLeastOnce()).twoArgumentMethod(1, 2);
            fail();
        } catch (WantedButNotInvoked e) {
            assertThat(e).hasMessageContaining("twoArgumentMethod(1, 2)");
        }
    }

    @Test
    public void should_print_method_when_matcher_used() throws Exception {
        try {
            verify(mock, atLeastOnce()).twoArgumentMethod(anyInt(), eq(100));
            fail();
        } catch (WantedButNotInvoked e) {
            String expectedMessage =
                    "\n"
                            + "Wanted but not invoked:"
                            + "\n"
                            + "iMethods.twoArgumentMethod(\n"
                            + "    <any integer>,\n"
                            + "    100\n"
                            + ");";
            assertThat(e).hasMessageContaining(expectedMessage);
        }
    }

    @Test
    public void should_print_method_when_missing_invocation_with_array_matcher() {
        mock.oneArray(new boolean[] {true, false, false});

        try {
            verify(mock).oneArray(aryEq(new boolean[] {false, false, false}));
            fail();
        } catch (ArgumentsAreDifferent e) {
            assertThat(e)
                    .hasMessageContaining("[false, false, false]")
                    .hasMessageContaining("[true, false, false]");
        }
    }

    @Test
    public void should_print_method_when_missing_invocation_with_vararg_matcher() {
        mock.varargsString(10, "xxx", "yyy", "zzz");

        try {
            verify(mock).varargsString(10, "111", "222", "333");
            fail();
        } catch (ArgumentsAreDifferent e) {
            assertThat(e).hasMessageContaining("111").hasMessageContaining("\"xxx\"");
        }
    }

    @Test
    public void should_print_method_when_missing_invocation_with_matcher() {
        mock.simpleMethod("foo");

        try {
            verify(mock).simpleMethod(matches("burrito from Exmouth"));
            fail();
        } catch (ArgumentsAreDifferent e) {
            assertThat(e)
                    .hasMessageContaining("matches(\"burrito from Exmouth\")")
                    .hasMessageContaining("\"foo\"");
        }
    }

    @Test
    public void should_print_null_arguments() throws Exception {
        mock.simpleMethod(null, (Integer) null);
        try {
            verify(mock).simpleMethod("test");
            fail();
        } catch (ArgumentsAreDifferent e) {
            assertThat(e).hasMessageContaining("simpleMethod(null, null);");
        }
    }

    @Test
    public void should_say_never_wanted_but_invoked() throws Exception {
        mock.simpleMethod(1);

        verify(mock, never()).simpleMethod(2);
        try {
            verify(mock, never()).simpleMethod(1);
            fail();
        } catch (NeverWantedButInvoked e) {
            assertThat(e)
                    .hasMessageContaining("Never wanted here:")
                    .hasMessageContaining("But invoked here:");
        }
    }

    @Test
    public void should_show_right_actual_method() throws Exception {
        mock.simpleMethod(9191);
        mock.simpleMethod("foo");

        try {
            verify(mock).simpleMethod("bar");
            fail();
        } catch (ArgumentsAreDifferent e) {
            assertThat(e).hasMessageContaining("bar").hasMessageContaining("foo");
        }
    }

    @Mock private IMethods iHavefunkyName;

    @Test
    public void should_print_field_name_when_annotations_used() throws Exception {
        iHavefunkyName.simpleMethod(10);

        try {
            verify(iHavefunkyName).simpleMethod(20);
            fail();
        } catch (ArgumentsAreDifferent e) {
            assertThat(e)
                    .hasMessageContaining("iHavefunkyName.simpleMethod(20)")
                    .hasMessageContaining("iHavefunkyName.simpleMethod(10)");
        }
    }

    @Test
    public void should_print_interactions_on_mock_when_ordinary_verification_fail()
            throws Exception {
        mock.otherMethod();
        mock.booleanReturningMethod();

        try {
            verify(mock).simpleMethod();
            fail();
        } catch (WantedButNotInvoked e) {
            //            assertContains("")
        }
    }

    @Mock private IMethods veeeeeeeeeeeeeeeeeeeeeeeerylongNameMock;

    @Test
    public void should_never_break_method_string_when_no_args_in_method() throws Exception {
        try {
            verify(veeeeeeeeeeeeeeeeeeeeeeeerylongNameMock).simpleMethod();
            fail();
        } catch (WantedButNotInvoked e) {
            assertThat(e)
                    .hasMessageContaining("veeeeeeeeeeeeeeeeeeeeeeeerylongNameMock.simpleMethod()");
        }
    }

    @Test
    public void
            should_print_method_name_and_arguments_of_other_interactions_with_different_methods()
                    throws Exception {
        try {
            mock.arrayMethod(new String[] {"a", "b", "c"});
            mock.forByte((byte) 25);

            verify(mock).threeArgumentMethod(12, new Foo(), "xx");
            fail();
        } catch (WantedButNotInvoked e) {
            assertThat(e)
                    .hasMessageContaining("iMethods.threeArgumentMethod(12, foo, \"xx\")")
                    .hasMessageContaining("iMethods.arrayMethod([\"a\", \"b\", \"c\"])")
                    .hasMessageContaining("iMethods.forByte((byte) 0x19)");
        }
    }

    @Test
    public void should_print_fully_qualified_name_when_arguments_classes_have_same_simple_name() {
        Date dateArg = new Date(0);
        String stringifiedDateArg = dateArg.toString();

        try {
            mock.overloadedMethodWithSameClassNameArguments(dateArg, new IMethods.Date(0));
            verify(mock).overloadedMethodWithSameClassNameArguments(new IMethods.Date(0), dateArg);
            fail();
        } catch (Throwable e) {
            String wanted =
                    String.format(
                            "\n"
                                    + "Argument(s) are different! Wanted:"
                                    + "\n"
                                    + "iMethods.overloadedMethodWithSameClassNameArguments("
                                    + "\n"
                                    + "    (org.mockitousage.IMethods.Date) 0,"
                                    + "\n"
                                    + "    (java.sql.Date) %s"
                                    + "\n"
                                    + ");",
                            stringifiedDateArg);

            assertThat(e).hasMessageContaining(wanted);

            String actual =
                    String.format(
                            "\n"
                                    + "Actual invocations have different arguments at positions [0, 1]:"
                                    + "\n"
                                    + "iMethods.overloadedMethodWithSameClassNameArguments("
                                    + "\n"
                                    + "    (java.sql.Date) %s,"
                                    + "\n"
                                    + "    (org.mockitousage.IMethods.Date) 0"
                                    + "\n"
                                    + ");",
                            stringifiedDateArg);
            assertThat(e).hasMessageContaining(actual);
        }
    }

    @Test
    public void
            should_not_print_fully_qualified_name_when_arguments_classes_have_different_simple_name() {
        try {
            mock.overloadedMethodWithDifferentClassNameArguments("string", 0);
            verify(mock).overloadedMethodWithDifferentClassNameArguments(0, "string");
            fail();
        } catch (Throwable e) {
            String wanted =
                    "\n"
                            + "Argument(s) are different! Wanted:"
                            + "\n"
                            + "iMethods.overloadedMethodWithDifferentClassNameArguments("
                            + "\n"
                            + "    0,"
                            + "\n"
                            + "    \"string\""
                            + "\n"
                            + ");";

            assertThat(e).hasMessageContaining(wanted);

            String actual =
                    "\n"
                            + "Actual invocations have different arguments at positions [0, 1]:"
                            + "\n"
                            + "iMethods.overloadedMethodWithDifferentClassNameArguments("
                            + "\n"
                            + "    \"string\","
                            + "\n"
                            + "    0"
                            + "\n"
                            + ");";
            assertThat(e).hasMessageContaining(actual);
        }
    }

    @Test
    public void
            should_print_fully_qualified_name_when_some_arguments_classes_have_same_simple_name() {
        Date dateArg = new Date(0);
        String stringifiedDateArg = dateArg.toString();

        try {
            mock.overloadedMethodWithSameClassNameArguments(
                    dateArg, "string", new IMethods.Date(0));
            verify(mock)
                    .overloadedMethodWithSameClassNameArguments(
                            new IMethods.Date(0), "string", dateArg);
            fail();
        } catch (Throwable e) {
            String wanted =
                    String.format(
                            "\n"
                                    + "Argument(s) are different! Wanted:"
                                    + "\n"
                                    + "iMethods.overloadedMethodWithSameClassNameArguments("
                                    + "\n"
                                    + "    (org.mockitousage.IMethods.Date) 0,"
                                    + "\n"
                                    + "    \"string\","
                                    + "\n"
                                    + "    (java.sql.Date) %s"
                                    + "\n"
                                    + ");",
                            stringifiedDateArg);

            assertThat(e).hasMessageContaining(wanted);

            String actual =
                    String.format(
                            "\n"
                                    + "Actual invocations have different arguments at positions [0, 2]:"
                                    + "\n"
                                    + "iMethods.overloadedMethodWithSameClassNameArguments("
                                    + "\n"
                                    + "    (java.sql.Date) %s,"
                                    + "\n"
                                    + "    \"string\","
                                    + "\n"
                                    + "    (org.mockitousage.IMethods.Date) 0"
                                    + "\n"
                                    + ");",
                            stringifiedDateArg);
            assertThat(e).hasMessageContaining(actual);
        }
    }

    @Test
    @Ignore("issue 380 related")
    public void should_print_method_name_and_arguments_of_other_interactions_of_same_method()
            throws Exception {
        try {
            mock.forByte((byte) 25);
            mock.forByte((byte) 12);

            verify(mock).forByte((byte) 42);
            fail();
        } catch (WantedButNotInvoked e) {
            assertThat(e)
                    .hasMessageContaining("iMethods.forByte(42)")
                    .hasMessageContaining("iMethods.forByte(25)")
                    .hasMessageContaining("iMethods.forByte(12)");
        }
    }

    @Test
    @Ignore("issue 380 related")
    public void test1() {
        AnInterface m = Mockito.mock(AnInterface.class);

        for (int i = 1; i <= 2; i++) {
            m.foo(i);
        }

        verify(m).foo(1);
        verify(m).foo(2);
        verify(m).foo(3); // XXX: doesn't mention the parameters of foo(1) and foo(2)
        verify(m).foo(4);
    }

    @Test
    @Ignore("issue 380 related")
    public void test2() {
        AnInterface m = Mockito.mock(AnInterface.class);

        for (int i = 1; i <= 4; i++) {
            m.foo(i);
        }

        verify(m).foo(1);
        verify(m).foo(2);
        verify(m).foo(5); // XXX: doesn't mention foo(4) at all
    }

    public interface AnInterface {
        void foo(int i);
    }
}
