/*
 *   Copyright 2016 the original author or authors.
 *
 *   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 org.powermock.api.mockito.internal.invocation;

import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.spi.support.InvocationSubstitute;
import org.powermock.reflect.Whitebox;

import java.util.regex.Matcher;

public class InvocationControlAssertionError {
    private static final String AT = "at";
    private static final String ERROR_LOCATION_MARKER = "->";
    private static final String COLON_NEWLINE = ":\n";
    private static final String HERE_TEXT = "here:\n";
    private static final String UNDESIRED_INVOCATION_TEXT = " Undesired invocation:";

    public static void updateErrorMessageForVerifyNoMoreInteractions(AssertionError errorToUpdate) {
        /*
         * VerifyNoMoreInteractions failed, we need to update the error message.
         */
        String verifyNoMoreInteractionsInvocation = null;
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        for (int i = stackTrace.length - 1; i >= 0; i--) {
            final StackTraceElement stackTraceElement = stackTrace[i];
            if (stackTraceElement.getClassName().equals(PowerMockito.class.getName())
                    && stackTraceElement.getMethodName().equals("verifyNoMoreInteractions")) {
                final int invocationStackTraceIndex;
                if (stackTrace[i + 1].getClassName().equals(PowerMockito.class.getName())
                        && stackTrace[i + 1].getMethodName().equals("verifyZeroInteractions")) {
                    invocationStackTraceIndex = i + 2;
                } else {
                    invocationStackTraceIndex = i + 1;
                }
                verifyNoMoreInteractionsInvocation = stackTrace[invocationStackTraceIndex].toString();
            }
        }

        if (verifyNoMoreInteractionsInvocation == null) {
            // Something unexpected happened, just return
            return;
        }
        String message = errorToUpdate.getMessage();
        StringBuilder builder = new StringBuilder();
        builder.append(message);
        final int indexOfFirstAt = message.indexOf(AT);
        final int startOfVerifyNoMoreInteractionsInvocation = indexOfFirstAt + AT.length() + 1;
        final int endOfVerifyNoMoreInteractionsInvocation = message.indexOf('\n', indexOfFirstAt + AT.length());
        builder.replace(startOfVerifyNoMoreInteractionsInvocation, endOfVerifyNoMoreInteractionsInvocation,
                verifyNoMoreInteractionsInvocation);
        builder.delete(builder.indexOf("\n", endOfVerifyNoMoreInteractionsInvocation + 1), builder.lastIndexOf("\n"));
        Whitebox.setInternalState(errorToUpdate, builder.toString());
    }

    public static void updateErrorMessageForMethodInvocation(AssertionError errorToUpdate) {
        /*
         * We failed to verify the new substitution mock. This happens when, for
         * example, the user has done something like
         * whenNew(MyClass.class).thenReturn(myMock).times(3) when in fact an
         * instance of MyClass has been created less or more times than 3.
         */
        Whitebox.setInternalState(errorToUpdate, "\n" + changeMessageContent(errorToUpdate.getMessage()));
    }

    public static void throwAssertionErrorForNewSubstitutionFailure(AssertionError oldError, Class<?> type) {
        /*
         * We failed to verify the new substitution mock. This happens when, for
         * example, the user has done something like
         * whenNew(MyClass.class).thenReturn(myMock).times(3) when in fact an
         * instance of MyClass has been created less or more times than 3.
         */
        final String newSubsitutionClassName = InvocationSubstitute.class.getSimpleName();
        final String newSubsitutionClassNameInMockito = newSubsitutionClassName.substring(0, 1).toLowerCase()
                + newSubsitutionClassName.substring(1);
        String message = oldError.getMessage();
        final String newSubsitutionMethodName = InvocationSubstitute.class.getDeclaredMethods()[0].getName();
        message = message.replaceAll(newSubsitutionClassNameInMockito + "." + newSubsitutionMethodName, Matcher
                .quoteReplacement(type.getName()));
        message = message.replaceAll("method", "constructor");
        throw new AssertionError(changeMessageContent(message));
    }

    private static String changeMessageContent(String message) {
        /*
         * Temp fix: Remove powermock internal "at locations" (points to which
         * line the expectation went wrong in Mockito). We should try to find
         * the real ones instead
         */
        StringBuilder builder = removeFailureLocations(message);
        // Remove "Undesired invocation:"
        removeText(builder, UNDESIRED_INVOCATION_TEXT);

        removeAndReplaceText(builder, HERE_TEXT, ' ');

        removeAndReplaceText(builder, COLON_NEWLINE, ' ');
        final String finalMessage = builder.toString().trim();
        return finalMessage;
    }

    private static StringBuilder removeFailureLocations(String message) {
        StringBuilder builder = new StringBuilder();
        builder.append(message);
        int indexOfBeginLocation = builder.indexOf(ERROR_LOCATION_MARKER);
        while (indexOfBeginLocation > 0) {
            int indexOfLocationEnd = builder.indexOf("\n", indexOfBeginLocation);
            builder.delete(indexOfBeginLocation, indexOfLocationEnd < 0 ? builder.length() : indexOfLocationEnd + 1);
            indexOfBeginLocation = builder.indexOf(ERROR_LOCATION_MARKER);
        }
        return builder;
    }

    private static void removeAndReplaceText(StringBuilder builder, String text, char appender) {
        int currentTextIndex = builder.indexOf(text);
        int previousTextIndex = 0;
        boolean isSingleConcat = true;
        while (currentTextIndex > 0) {
            previousTextIndex = currentTextIndex;
            builder.delete(currentTextIndex, currentTextIndex + text.length());
            currentTextIndex = builder.indexOf(text);
            if (isLastFinding(currentTextIndex) && !isSingleConcat) {
                builder.replace(builder.length(), builder.length(), ".");
            } else {
                builder.replace(previousTextIndex, previousTextIndex + 1, String.valueOf(
                        builder.charAt(previousTextIndex)).toLowerCase());
                builder.insert(previousTextIndex, String.valueOf(appender));
                currentTextIndex++;
                isSingleConcat = false;
            }
        }
    }

    private static boolean isLastFinding(int index) {
        return index < 0;
    }

    private static void removeText(StringBuilder builder, String text) {
        int textIndex = builder.indexOf(text);
        while (textIndex > 0) {
            builder.delete(textIndex, textIndex + text.length());
            textIndex = builder.indexOf(text);
        }
    }

}
