package org.apache.maven.surefire.report;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import org.apache.maven.surefire.api.report.SafeThrowable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static java.util.Arrays.asList;
import static java.util.Collections.reverse;
import static org.apache.maven.surefire.shared.utils.StringUtils.chompLast;
import static org.apache.maven.surefire.shared.utils.StringUtils.isNotEmpty;

/**
 * @author Kristian Rosenvold
 */
@SuppressWarnings( "ThrowableResultOfMethodCallIgnored" )
public class SmartStackTraceParser
{
    private final SafeThrowable throwable;

    private final StackTraceElement[] stackTrace;

    private final String testClassName;

    private final Class<?> testClass;

    private final String testMethodName;

    public SmartStackTraceParser( String testClassName, Throwable throwable, String testMethodName )
    {
        this.testMethodName = testMethodName;
        this.testClassName = testClassName;
        testClass = toClass( testClassName );
        this.throwable = new SafeThrowable( throwable );
        stackTrace = throwable.getStackTrace();
    }

    private static Class<?> toClass( String name )
    {
        try
        {
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            return classLoader == null ? null : classLoader.loadClass( name );
        }
        catch ( ClassNotFoundException e )
        {
            return null;
        }
    }

    private static String toSimpleClassName( String className )
    {
        int i = className.lastIndexOf( "." );
        return className.substring( i + 1 );
    }

    @SuppressWarnings( "ThrowableResultOfMethodCallIgnored" )
    public String getString()
    {
        if ( testClass == null )
        {
            return throwable.getLocalizedMessage();
        }

        final StringBuilder result = new StringBuilder();
        final List<StackTraceElement> stackTraceElements = focusOnClass( stackTrace, testClass );
        reverse( stackTraceElements );
        final String testClassSimpleName = toSimpleClassName( testClassName );
        if ( stackTraceElements.isEmpty() )
        {
            result.append( testClassSimpleName );
            if ( isNotEmpty( testMethodName ) )
            {
                result.append( "." )
                    .append( testMethodName );
            }
        }
        else
        {
            for ( int i = 0, size = stackTraceElements.size(); i < size; i++ )
            {
                final StackTraceElement stackTraceElement = stackTraceElements.get( i );
                final boolean isTestClassName = stackTraceElement.getClassName().equals( testClassName );
                if ( i == 0 )
                {
                    result.append( testClassSimpleName )
                            .append( isTestClassName ? '.' : '>' );
                }

                if ( !isTestClassName )
                {
                    result.append( toSimpleClassName( stackTraceElement.getClassName() ) )
                        .append( '.' );
                }

                result.append( stackTraceElement.getMethodName() )
                    .append( ':' )
                    .append( stackTraceElement.getLineNumber() )
                    .append( "->" );
            }

            if ( result.length() >= 2 )
            {
                result.setLength( result.length() - 2 );
            }
        }

        final Throwable target = throwable.getTarget();
        final Class<?> excType = target.getClass();
        final String excClassName = excType.getName();
        final String msg = throwable.getMessage();

        if ( ! ( target instanceof AssertionError
                || "junit.framework.AssertionFailedError".equals( excClassName )
                || "junit.framework.ComparisonFailure".equals( excClassName )
                || excClassName.startsWith( "org.opentest4j." ) ) )
        {
            result.append( rootIsInclass() ? " " : " » " )
                  .append( toMinimalThrowableMiniMessage( excType ) );
        }

        if ( isNotEmpty( msg ) )
        {
            result.append( ' ' )
                  .append( msg );
        }
        return result.toString();
    }

    private static String toMinimalThrowableMiniMessage( Class<?> excType )
    {
        String name = excType.getSimpleName();
        if ( name.endsWith( "Exception" ) )
        {
            return chompLast( name, "Exception" );
        }
        if ( name.endsWith( "Error" ) )
        {
            return chompLast( name, "Error" );
        }
        return name;
    }

    private boolean rootIsInclass()
    {
        return stackTrace != null && stackTrace.length > 0 && stackTrace[0].getClassName().equals( testClassName );
    }

    private static List<StackTraceElement> focusOnClass( StackTraceElement[] stackTrace, Class<?> clazz )
    {
        if ( stackTrace == null )
        {
            return Collections.emptyList();
        }
        List<StackTraceElement> result = new ArrayList<>();
        for ( StackTraceElement element : stackTrace )
        {
            if ( element != null && isInSupers( clazz, element.getClassName() ) )
            {
                result.add( element );
            }
        }
        return result;
    }

    private static boolean isInSupers( Class<?> testClass, String lookFor )
    {
        if ( lookFor.startsWith( "junit.framework." ) )
        {
            return false;
        }
        while ( !testClass.getName().equals( lookFor ) && testClass.getSuperclass() != null )
        {
            testClass = testClass.getSuperclass();
        }
        return testClass.getName().equals( lookFor );
    }

    static Throwable findTopmostWithClass( final Throwable t, StackTraceFilter filter )
    {
        Throwable n = t;
        do
        {
            if ( containsClassName( n.getStackTrace(), filter ) )
            {
                return n;
            }

            n = n.getCause();
        }
        while ( n != null );
        return t;
    }

    public static String stackTraceWithFocusOnClassAsString( Throwable t, String className )
    {
        StackTraceFilter filter = new ClassNameStackTraceFilter( className );
        Throwable topmost = findTopmostWithClass( t, filter );
        List<StackTraceElement> stackTraceElements = focusInsideClass( topmost.getStackTrace(), filter );
        String s = causeToString( topmost.getCause(), filter );
        return toString( t, stackTraceElements, filter ) + s;
    }

    static List<StackTraceElement> focusInsideClass( StackTraceElement[] stackTrace, StackTraceFilter filter )
    {
        List<StackTraceElement> result = new ArrayList<>();
        for ( StackTraceElement element : stackTrace )
        {
            if ( filter.matches( element ) )
            {
                result.add( element );
            }
        }
        return result;
    }

    private static boolean containsClassName( StackTraceElement[] stackTrace, StackTraceFilter filter )
    {
        for ( StackTraceElement element : stackTrace )
        {
            if ( filter.matches( element ) )
            {
                return true;
            }
        }
        return false;
    }

    private static String causeToString( Throwable cause, StackTraceFilter filter )
    {
        StringBuilder resp = new StringBuilder();
        while ( cause != null )
        {
            resp.append( "Caused by: " );
            resp.append( toString( cause, asList( cause.getStackTrace() ), filter ) );
            cause = cause.getCause();
        }
        return resp.toString();
    }

    private static String toString( Throwable t, Iterable<StackTraceElement> elements, StackTraceFilter filter )
    {
        StringBuilder result = new StringBuilder();
        if ( t != null )
        {
            result.append( t.getClass().getName() );
            String msg = t.getMessage();
            if ( msg != null )
            {
                result.append( ": " );
                if ( isMultiLine( msg ) )
                {
                    // SUREFIRE-986
                    result.append( '\n' );
                }
                result.append( msg );
            }
            result.append( '\n' );
        }

        for ( StackTraceElement element : elements )
        {
            if ( filter.matches( element ) )
            {
                result.append( "\tat " )
                        .append( element )
                        .append( '\n' );
            }
        }
        return result.toString();
    }

    private static boolean isMultiLine( String msg )
    {
        int countNewLines = 0;
        for ( int i = 0, length = msg.length(); i < length; i++ )
        {
            if ( msg.charAt( i ) == '\n' )
            {
                if ( ++countNewLines == 2 )
                {
                    break;
                }
            }
        }
        return countNewLines > 1 || countNewLines == 1 && !msg.trim().endsWith( "\n" );
    }
}
