/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.mockito.inline.extended.tests;

import static android.provider.Settings.Global.DEVICE_NAME;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockingDetails;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.reset;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;

import android.content.ContentResolver;
import android.provider.Settings;

import androidx.test.InstrumentationRegistry;

import org.junit.Test;
import org.mockito.MockingDetails;
import org.mockito.MockitoSession;
import org.mockito.exceptions.misusing.MissingMethodInvocationException;
import org.mockito.quality.Strictness;

public class MockStatic {
    private static class SuperClass {
        final String returnA() {
            return "superA";
        }

        static String returnB() {
            return "superB";
        }

        static String returnC() {
            return "superC";
        }
    }

    private static final class SubClass extends SuperClass {
        static String recorded = null;

        static String returnC() {
            return "subC";
        }

        static final String record(String toRecord) {
            recorded = toRecord;
            return "record";
        }
    }

    private static class NoDefaultConstructorClass {
        private static int mLastId;

        NoDefaultConstructorClass(int id) {
            mLastId = id;
        }

        static int getLastId() {
            return mLastId;
        }
    }

    @Test
    public void spyStatic() throws Exception {
        ContentResolver resolver = InstrumentationRegistry.getTargetContext().getContentResolver();
        String deviceName = Settings.Global.getString(resolver, DEVICE_NAME);

        MockitoSession session = mockitoSession().spyStatic(Settings.Global.class).startMocking();
        try {
            // Cannot call when(Settings.getString(any(ContentResolver.class), eq("...")))
            // as any(ContentResolver.class) returns null which makes getString fail. Hence need to
            // use less lambda API
            doReturn("23").when(() -> Settings.Global.getString(any
                    (ContentResolver.class), eq("twenty three")));

            doReturn(42).when(() -> Settings.Global.getInt(any
                    (ContentResolver.class), eq("fourty two")));

            // Make sure behavior is changed
            assertEquals("23", Settings.Global.getString(resolver, "twenty three"));
            assertEquals(42, Settings.Global.getInt(resolver, "fourty two"));

            // Make sure non-mocked methods work as before
            assertEquals(deviceName, Settings.Global.getString(resolver, DEVICE_NAME));
        } finally {
            session.finishMocking();
        }
    }

    @Test
    public void spyStaticOnObjectWithNoDefaultConstructor() throws Exception {
        new NoDefaultConstructorClass(23);

        MockitoSession session = mockitoSession().spyStatic(NoDefaultConstructorClass.class)
                .startMocking();
        try {
            // Starting the spying hasn't change the static state of the class.
            assertEquals(23, NoDefaultConstructorClass.getLastId());

            when(NoDefaultConstructorClass.getLastId()).thenReturn(42);
            assertEquals(42, NoDefaultConstructorClass.getLastId());
        } finally {
            session.finishMocking();
        }
    }

    @Test
    public void mockStatic() throws Exception {
        ContentResolver resolver = InstrumentationRegistry.getTargetContext().getContentResolver();
        String deviceName = Settings.Global.getString(resolver, DEVICE_NAME);

        MockitoSession session = mockitoSession().mockStatic(Settings.Global.class).startMocking();
        try {
            // By default all static methods of the mocked class should return null/0/false
            assertNull(Settings.Global.getString(resolver, DEVICE_NAME));

            when(Settings.Global.getString(any(ContentResolver.class), eq(DEVICE_NAME)))
                    .thenReturn("This is a test");

            // Make sure behavior is changed
            assertEquals("This is a test", Settings.Global.getString(resolver, DEVICE_NAME));
        } finally {
            session.finishMocking();
        }

        // Once the mocking is removed, the behavior should be back to normal
        assertEquals(deviceName, Settings.Global.getString(resolver, DEVICE_NAME));
    }

    @Test
    public void mockOverriddenStaticMethod() throws Exception {
        MockitoSession session = mockitoSession().mockStatic(SubClass.class).startMocking();
        try {
            // By default all static methods of the mocked class should return the default answers
            assertNull(SubClass.returnB());
            assertNull(SubClass.returnC());

            // Super class is not mocked
            assertEquals("superB", SuperClass.returnB());
            assertEquals("superC", SuperClass.returnC());

            when(SubClass.returnB()).thenReturn("fakeB");
            when(SubClass.returnC()).thenReturn("fakeC");

            // Make sure behavior is changed
            assertEquals("fakeB", SubClass.returnB());
            assertEquals("fakeC", SubClass.returnC());

            // Super class should not be affected
            assertEquals("superB", SuperClass.returnB());
            assertEquals("superC", SuperClass.returnC());
        } finally {
            session.finishMocking();
        }

        // Mocking should be stopped
        assertEquals("superB", SubClass.returnB());
        assertEquals("subC", SubClass.returnC());
    }

    @Test
    public void mockSuperMethod() throws Exception {
        MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
        try {
            // By default all static methods of the mocked class should return the default answers
            assertNull(SuperClass.returnB());
            assertNull(SuperClass.returnC());

            // Sub class should not be affected
            assertEquals("superB", SubClass.returnB());
            assertEquals("subC", SubClass.returnC());

            when(SuperClass.returnB()).thenReturn("fakeB");
            when(SuperClass.returnC()).thenReturn("fakeC");

            // Make sure behavior is changed
            assertEquals("fakeB", SuperClass.returnB());
            assertEquals("fakeC", SuperClass.returnC());

            // Sub class should not be affected
            assertEquals("superB", SubClass.returnB());
            assertEquals("subC", SubClass.returnC());
        } finally {
            session.finishMocking();
        }

        // Mocking should be stopped
        assertEquals("superB", SuperClass.returnB());
        assertEquals("superC", SuperClass.returnC());
    }

    @Test(expected = MissingMethodInvocationException.class)
    public void nonMockedTest() throws Exception {
        when(SuperClass.returnB()).thenReturn("fakeB");
    }

    @Test
    public void resetMock() throws Exception {
        MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
        try {
            assertNull(SuperClass.returnB());

            when(SuperClass.returnB()).thenReturn("fakeB");
            assertEquals("fakeB", SuperClass.returnB());

            reset(staticMockMarker(SuperClass.class));
            assertNull(SuperClass.returnB());
        } finally {
            session.finishMocking();
        }
    }

    @Test
    public void resetSpy() throws Exception {
        MockitoSession session = mockitoSession().spyStatic(SuperClass.class).startMocking();
        try {
            assertEquals("superB", SuperClass.returnB());

            when(SuperClass.returnB()).thenReturn("fakeB");
            assertEquals("fakeB", SuperClass.returnB());

            reset(staticMockMarker(SuperClass.class));
            assertEquals("superB", SuperClass.returnB());
        } finally {
            session.finishMocking();
        }
    }

    @Test
    public void staticMockingIsSeparateFromNonStaticMocking() throws Exception {
        SuperClass objA = new SuperClass();
        SuperClass objB;

        MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
        try {
            assertNull(SuperClass.returnB());
            assertNull(objA.returnB());

            objB = mock(SuperClass.class);

            assertEquals("superA", objA.returnA());

            // Any kind of static method method call should be mocked
            assertNull(objB.returnA());

            assertNull(SuperClass.returnB());
            assertNull(objA.returnB());
            assertNull(objB.returnB());
        } finally {
            session.finishMocking();
        }

        assertEquals("superA", objA.returnA());
        assertNull(objB.returnA());

        // Any kind of static method method call should _not_ be mocked
        assertEquals("superB", SuperClass.returnB());
        assertEquals("superB", objA.returnB());
        assertEquals("superB", objB.returnB());
    }

    @Test
    public void mockWithTwoClasses() throws Exception {
        MockitoSession session = mockitoSession().mockStatic(SuperClass.class)
                .mockStatic(SubClass.class).startMocking();
        try {
            when(SuperClass.returnB()).thenReturn("fakeB");
            assertEquals("fakeB", SuperClass.returnB());

            when(SubClass.returnC()).thenReturn("fakeC");
            assertEquals("fakeC", SubClass.returnC());
        } finally {
            session.finishMocking();
        }
    }

    @Test
    public void doReturnMockWithTwoClasses() throws Exception {
        MockitoSession session = mockitoSession().mockStatic(SuperClass.class)
                .mockStatic(SubClass.class).startMocking();
        try {
            doReturn("fakeB").when(SuperClass::returnB);
            assertEquals("fakeB", SuperClass.returnB());

            doReturn("fakeD").when(() -> SubClass.record("test"));
            assertEquals("fakeD", SubClass.record("test"));
        } finally {
            session.finishMocking();
        }
    }

    @Test
    public void doReturnTwice() throws Exception {
        MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
        try {
            doReturn("fakeB").doReturn("fakeB2").when(SuperClass::returnB);
            assertEquals("fakeB", SuperClass.returnB());
            assertEquals("fakeB2", SuperClass.returnB());
        } finally {
            session.finishMocking();
        }
    }

    @Test
    public void doReturnSpyHasNoSideEffect() throws Exception {
        MockitoSession session = mockitoSession().spyStatic(SubClass.class).startMocking();
        try {
            SubClass.recorded = null;
            SubClass.record("no sideeffect");
            assertEquals("no sideeffect", SubClass.recorded);

            doReturn("faceRecord").when(() -> SubClass.record(eq("test")));
            // Verify that there was no side effect as the lambda gets intercepted
            assertEquals("no sideeffect", SubClass.recorded);

            assertEquals("faceRecord", SubClass.record("test"));
            // Verify that there was no side effect as the method is stubbed
            assertEquals("no sideeffect", SubClass.recorded);
        } finally {
            session.finishMocking();
        }
    }

    @Test
    public void onlyOneMethodCallDuringStubbing() throws Exception {
        MockitoSession session = mockitoSession().strictness(Strictness.LENIENT)
                .spyStatic(SuperClass.class).startMocking();
        try {
            try {
                doReturn("").when(() -> {
                    SuperClass.returnB();
                    SuperClass.returnC();
                });
                fail();
            } catch (IllegalArgumentException e) {
                assertTrue(e.getMessage(), e.getMessage().contains("returnB"));
                assertTrue(e.getMessage(), e.getMessage().contains("returnC"));

                assertFalse(e.getMessage(), e.getMessage().contains("returnA"));
            }
        } finally {
            session.finishMocking();
        }
    }

    @Test
    public void atLeastOneMethodCallDuringStubbing() throws Exception {
        Exception atLeastOneMethodCallException = null;

        try {
            MockitoSession session = mockitoSession().spyStatic(SuperClass.class).startMocking();
            try {
                try {
                    doReturn("").when(() -> {
                    });
                    fail();
                } catch (IllegalArgumentException expected) {
                    atLeastOneMethodCallException = expected;
                }
            } finally {
                session.finishMocking();
            }
        } catch (Throwable ignored) {
            // We don't want to test exceptions form MockitoSession
        }

        assertNotNull(atLeastOneMethodCallException);
    }

    @Test
    public void clearInvocationsRemovedInvocations() throws Exception {
        MockitoSession session = mockitoSession().mockStatic(SuperClass.class).startMocking();
        try {
            SuperClass.returnB();
            clearInvocations(staticMockMarker(SuperClass.class));
            verifyNoMoreInteractions(staticMockMarker(SuperClass.class));
        } finally {
            session.finishMocking();
        }
    }

    @Test
    public void verifyMockingDetails() throws Exception {
        MockitoSession session = mockitoSession().mockStatic(SuperClass.class)
                .spyStatic(SubClass.class).startMocking();
        try {
            when(SuperClass.returnB()).thenReturn("fakeB");
            SuperClass.returnB();
            SuperClass.returnC();

            MockingDetails superClassDetails = mockingDetails(staticMockMarker(SuperClass.class));
            assertTrue(superClassDetails.isMock());
            assertFalse(superClassDetails.isSpy());
            assertEquals(2, superClassDetails.getInvocations().size());
            assertEquals(1, superClassDetails.getStubbings().size());

            MockingDetails subClassDetails = mockingDetails(staticMockMarker(SubClass.class));
            assertTrue(subClassDetails.isMock());
            assertTrue(subClassDetails.isSpy());
        } finally {
            session.finishMocking();
        }
    }
}
