/*
 * Copyright (C) 2018 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;

import org.mockito.Mockito;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;

import java.util.ArrayList;
import java.util.HashMap;

import static com.android.dx.mockito.inline.InlineStaticMockMaker.mockingInProgressClass;

/**
 * Same as {@link MockitoSession} but used when static methods are also stubbed.
 */
@UnstableApi
public class StaticMockitoSession implements MockitoSession {
    /**
     * For each class where static mocking is enabled there is one marker object.
     */
    private static final HashMap<Class, Object> classToMarker = new HashMap<>();

    private final MockitoSession instanceSession;
    private final ArrayList<Class<?>> staticMocks = new ArrayList<>(0);

    StaticMockitoSession(MockitoSession instanceSession) {
        ExtendedMockito.addSession(this);
        this.instanceSession = instanceSession;
    }

    @Override
    public void setStrictness(Strictness strictness) {
        instanceSession.setStrictness(strictness);
    }

    /**
     * {@inheritDoc}
     * <p><b>Extension:</b> This also resets all stubbing of static methods set up in the
     * {@link ExtendedMockito#mockitoSession() builder} of the session.
     */
    @Override
    public void finishMocking() {
        finishMocking(null);
    }

    /**
     * {@inheritDoc}
     * <p><b>Extension:</b> This also resets all stubbing of static methods set up in the
     * {@link ExtendedMockito#mockitoSession() builder} of the session.
     */
    @Override
    public void finishMocking(Throwable failure) {
        try {
            instanceSession.finishMocking(failure);
        } finally {
            for (Class<?> clazz : staticMocks) {
                mockingInProgressClass.set(clazz);
                try {
                    Mockito.reset(ExtendedMockito.staticMockMarker(clazz));
                } finally {
                    mockingInProgressClass.remove();
                }
                classToMarker.remove(clazz);
            }

            ExtendedMockito.removeSession(this);
        }
    }

    /**
     * Init mocking for a class.
     *
     * @param mocking Description and settings of the mocking
     * @param <T>     The class to mock
     */
    <T> void mockStatic(StaticMocking<T> mocking) {
        if (ExtendedMockito.staticMockMarker(mocking.clazz) != null) {
            throw new IllegalArgumentException(mocking.clazz + " is already mocked");
        }

        mockingInProgressClass.set(mocking.clazz);
        try {
            classToMarker.put(mocking.clazz, mocking.markerSupplier.get());
        } finally {
            mockingInProgressClass.remove();
        }

        staticMocks.add(mocking.clazz);
    }

    /**
     * Get marker for a mocked/spies class or {@code null}.
     *
     * @param clazz The class that is mocked
     * @return marker for a mocked class or {@code null} if class is not mocked in this session
     * @see ExtendedMockito#staticMockMarker(Class)
     */
    @SuppressWarnings("unchecked")
    <T> T staticMockMarker(Class<T> clazz) {
        return (T) classToMarker.get(clazz);
    }
}