package org.apache.maven.plugins.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 java.io.File;
import java.text.NumberFormat;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.maven.doxia.markup.HtmlMarkup;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
import org.apache.maven.doxia.util.DoxiaUtils;
import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
import org.apache.maven.reporting.MavenReportException;

import static org.apache.maven.doxia.markup.HtmlMarkup.A;
import static org.apache.maven.doxia.sink.Sink.JUSTIFY_LEFT;
import static org.apache.maven.doxia.sink.SinkEventAttributes.CLASS;
import static org.apache.maven.doxia.sink.SinkEventAttributes.HREF;
import static org.apache.maven.doxia.sink.SinkEventAttributes.ID;
import static org.apache.maven.doxia.sink.SinkEventAttributes.STYLE;
import static org.apache.maven.doxia.sink.SinkEventAttributes.TYPE;

/**
 * This generator creates HTML Report from Surefire and Failsafe XML Report.
 */
public final class SurefireReportGenerator
{
    private static final int LEFT = JUSTIFY_LEFT;
    private static final Object[] TAG_TYPE_START = { HtmlMarkup.TAG_TYPE_START };
    private static final Object[] TAG_TYPE_END = { HtmlMarkup.TAG_TYPE_END };

    private final SurefireReportParser report;
    private final boolean showSuccess;
    private final String xrefLocation;
    private List<ReportTestSuite> testSuites;

    public SurefireReportGenerator( List<File> reportsDirectories, Locale locale, boolean showSuccess,
                                    String xrefLocation, ConsoleLogger consoleLogger )
    {
        report = new SurefireReportParser( reportsDirectories, locale, consoleLogger );
        this.showSuccess = showSuccess;
        this.xrefLocation = xrefLocation;
    }

    public void doGenerateReport( LocalizedProperties bundle, Sink sink )
        throws MavenReportException
    {
        testSuites = report.parseXMLReportFiles();

        sink.head();

        sink.title();
        sink.text( bundle.getReportHeader() );
        sink.title_();

        sink.head_();

        sink.body();

        SinkEventAttributeSet atts = new SinkEventAttributeSet();
        atts.addAttribute( TYPE, "application/javascript" );
        sink.unknown( "script", new Object[]{ HtmlMarkup.TAG_TYPE_START }, atts );
        sink.unknown( "cdata", new Object[]{ HtmlMarkup.CDATA_TYPE, javascriptToggleDisplayCode() }, null );
        sink.unknown( "script", new Object[]{ HtmlMarkup.TAG_TYPE_END }, null );

        sink.section1();
        sink.sectionTitle1();
        sink.text( bundle.getReportHeader() );
        sink.sectionTitle1_();
        sink.section1_();

        constructSummarySection( bundle, sink );

        Map<String, List<ReportTestSuite>> suitePackages = report.getSuitesGroupByPackage( testSuites );
        if ( !suitePackages.isEmpty() )
        {
            constructPackagesSection( bundle, sink, suitePackages );
        }

        if ( !testSuites.isEmpty() )
        {
            constructTestCasesSection( bundle, sink );
        }

        List<ReportTestCase> failureList = report.getFailureDetails( testSuites );
        if ( !failureList.isEmpty() )
        {
            constructFailureDetails( sink, bundle, failureList );
        }

        sink.body_();

        sink.flush();

        sink.close();
    }

    private void constructSummarySection( LocalizedProperties bundle, Sink sink )
    {
        Map<String, String> summary = report.getSummary( testSuites );

        sink.section1();
        sinkAnchor( sink, "Summary" );
        sink.sectionTitle1();
        sink.text( bundle.getReportLabelSummary() );
        sink.sectionTitle1_();

        constructHotLinks( sink, bundle );

        sinkLineBreak( sink );

        sink.table();

        sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, false );

        sink.tableRow();

        sinkHeader( sink, bundle.getReportLabelTests() );

        sinkHeader( sink, bundle.getReportLabelErrors() );

        sinkHeader( sink, bundle.getReportLabelFailures() );

        sinkHeader( sink, bundle.getReportLabelSkipped() );

        sinkHeader( sink, bundle.getReportLabelSuccessRate() );

        sinkHeader( sink, bundle.getReportLabelTime() );

        sink.tableRow_();

        sink.tableRow();

        sinkCell( sink, summary.get( "totalTests" ) );

        sinkCell( sink, summary.get( "totalErrors" ) );

        sinkCell( sink, summary.get( "totalFailures" ) );

        sinkCell( sink, summary.get( "totalSkipped" ) );

        sinkCell( sink, summary.get( "totalPercentage" ) + "%" );

        sinkCell( sink, summary.get( "totalElapsedTime" ) );

        sink.tableRow_();

        sink.tableRows_();

        sink.table_();

        sink.lineBreak();

        sink.paragraph();
        sink.text( bundle.getReportTextNode1() );
        sink.paragraph_();

        sinkLineBreak( sink );

        sink.section1_();
    }

    private void constructPackagesSection( LocalizedProperties bundle, Sink sink,
                                           Map<String, List<ReportTestSuite>> suitePackages )
    {
        NumberFormat numberFormat = report.getNumberFormat();

        sink.section1();
        sinkAnchor( sink, "Package_List" );
        sink.sectionTitle1();
        sink.text( bundle.getReportLabelPackageList() );
        sink.sectionTitle1_();

        constructHotLinks( sink, bundle );

        sinkLineBreak( sink );

        sink.table();

        sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, false );

        sink.tableRow();

        sinkHeader( sink, bundle.getReportLabelPackage() );

        sinkHeader( sink, bundle.getReportLabelTests() );

        sinkHeader( sink, bundle.getReportLabelErrors() );

        sinkHeader( sink, bundle.getReportLabelFailures() );

        sinkHeader( sink, bundle.getReportLabelSkipped() );

        sinkHeader( sink, bundle.getReportLabelSuccessRate() );

        sinkHeader( sink, bundle.getReportLabelTime() );

        sink.tableRow_();

        for ( Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet() )
        {
            sink.tableRow();

            String packageName = entry.getKey();

            List<ReportTestSuite> testSuiteList = entry.getValue();

            Map<String, String> packageSummary = report.getSummary( testSuiteList );

            sinkCellLink( sink, packageName, "#" + packageName );

            sinkCell( sink, packageSummary.get( "totalTests" ) );

            sinkCell( sink, packageSummary.get( "totalErrors" ) );

            sinkCell( sink, packageSummary.get( "totalFailures" ) );

            sinkCell( sink, packageSummary.get( "totalSkipped" ) );

            sinkCell( sink, packageSummary.get( "totalPercentage" ) + "%" );

            sinkCell( sink, packageSummary.get( "totalElapsedTime" ) );

            sink.tableRow_();
        }

        sink.tableRows_();

        sink.table_();

        sink.lineBreak();

        sink.paragraph();
        sink.text( bundle.getReportTextNode2() );
        sink.paragraph_();

        for ( Map.Entry<String, List<ReportTestSuite>> entry : suitePackages.entrySet() )
        {
            String packageName = entry.getKey();

            List<ReportTestSuite> testSuiteList = entry.getValue();

            sink.section2();
            sinkAnchor( sink, packageName );
            sink.sectionTitle2();
            sink.text( packageName );
            sink.sectionTitle2_();

            boolean showTable = false;

            for ( ReportTestSuite suite : testSuiteList )
            {
                if ( showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0 )
                {
                    showTable = true;

                    break;
                }
            }

            if ( showTable )
            {
                sink.table();

                sink.tableRows( new int[]{ LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT, LEFT }, false );

                sink.tableRow();

                sinkHeader( sink, "" );

                sinkHeader( sink, bundle.getReportLabelClass() );

                sinkHeader( sink, bundle.getReportLabelTests() );

                sinkHeader( sink, bundle.getReportLabelErrors() );

                sinkHeader( sink, bundle.getReportLabelFailures() );

                sinkHeader( sink, bundle.getReportLabelSkipped() );

                sinkHeader( sink, bundle.getReportLabelSuccessRate() );

                sinkHeader( sink, bundle.getReportLabelTime() );

                sink.tableRow_();

                for ( ReportTestSuite suite : testSuiteList )
                {
                    if ( showSuccess || suite.getNumberOfErrors() != 0 || suite.getNumberOfFailures() != 0 )
                    {
                        constructTestSuiteSection( sink, numberFormat, suite );
                    }
                }

                sink.tableRows_();

                sink.table_();
            }

            sink.section2_();
        }

        sinkLineBreak( sink );

        sink.section1_();
    }

    private void constructTestSuiteSection( Sink sink, NumberFormat numberFormat, ReportTestSuite suite )
    {
        sink.tableRow();

        sink.tableCell();

        sink.link( "#" + suite.getPackageName() + '.' + suite.getName() );

        if ( suite.getNumberOfErrors() > 0 )
        {
            sinkIcon( "error", sink );
        }
        else if ( suite.getNumberOfFailures() > 0 )
        {
            sinkIcon( "junit.framework", sink );
        }
        else if ( suite.getNumberOfSkipped() > 0 )
        {
            sinkIcon( "skipped", sink );
        }
        else
        {
            sinkIcon( "success", sink );
        }

        sink.link_();

        sink.tableCell_();

        sinkCellLink( sink, suite.getName(), "#" + suite.getPackageName() + '.' + suite.getName() );

        sinkCell( sink, Integer.toString( suite.getNumberOfTests() ) );

        sinkCell( sink, Integer.toString( suite.getNumberOfErrors() ) );

        sinkCell( sink, Integer.toString( suite.getNumberOfFailures() ) );

        sinkCell( sink, Integer.toString( suite.getNumberOfSkipped() ) );

        String percentage = report.computePercentage( suite.getNumberOfTests(), suite.getNumberOfErrors(),
                                                      suite.getNumberOfFailures(), suite.getNumberOfSkipped() );
        sinkCell( sink, percentage + "%" );

        sinkCell( sink, numberFormat.format( suite.getTimeElapsed() ) );

        sink.tableRow_();
    }

    private void constructTestCasesSection( LocalizedProperties bundle, Sink sink )
    {
        NumberFormat numberFormat = report.getNumberFormat();

        sink.section1();
        sinkAnchor( sink, "Test_Cases" );
        sink.sectionTitle1();
        sink.text( bundle.getReportLabelTestCases() );
        sink.sectionTitle1_();

        constructHotLinks( sink, bundle );

        for ( ReportTestSuite suite : testSuites )
        {
            List<ReportTestCase> testCases = suite.getTestCases();

            if ( !testCases.isEmpty() )
            {
                sink.section2();
                sinkAnchor( sink, suite.getPackageName() + '.' + suite.getName() );
                sink.sectionTitle2();
                sink.text( suite.getName() );
                sink.sectionTitle2_();

                boolean showTable = false;

                for ( ReportTestCase testCase : testCases )
                {
                    if ( !testCase.isSuccessful() || showSuccess )
                    {
                        showTable = true;

                        break;
                    }
                }

                if ( showTable )
                {
                    sink.table();

                    sink.tableRows( new int[]{ LEFT, LEFT, LEFT }, false );

                    for ( ReportTestCase testCase : testCases )
                    {
                        if ( !testCase.isSuccessful() || showSuccess )
                        {
                            constructTestCaseSection( sink, numberFormat, testCase );
                        }
                    }

                    sink.tableRows_();

                    sink.table_();
                }

                sink.section2_();
            }
        }

        sinkLineBreak( sink );

        sink.section1_();
    }

    private static void constructTestCaseSection( Sink sink, NumberFormat numberFormat, ReportTestCase testCase )
    {
        sink.tableRow();

        sink.tableCell();

        if ( testCase.getFailureType() != null )
        {
            sink.link( "#" + toHtmlId( testCase.getFullName() ) );

            sinkIcon( testCase.getFailureType(), sink );

            sink.link_();
        }
        else
        {
            sinkIcon( "success", sink );
        }

        sink.tableCell_();

        if ( !testCase.isSuccessful() )
        {
            sink.tableCell();
            sinkAnchor( sink, "TC_" + toHtmlId( testCase.getFullName() ) );

            sinkLink( sink, testCase.getName(), "#" + toHtmlId( testCase.getFullName() ) );

            SinkEventAttributeSet atts = new SinkEventAttributeSet();
            atts.addAttribute( CLASS, "detailToggle" );
            atts.addAttribute( STYLE, "display:inline" );
            sink.unknown( "div", TAG_TYPE_START, atts );

            sinkLink( sink, "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" );

            atts = new SinkEventAttributeSet();
            atts.addAttribute( STYLE, "display:inline;" );
            atts.addAttribute( ID, toHtmlId( testCase.getFullName() ) + "-off" );
            sink.unknown( "span", TAG_TYPE_START, atts );
            sink.text( " + " );
            sink.unknown( "span", TAG_TYPE_END, null );

            atts = new SinkEventAttributeSet();
            atts.addAttribute( STYLE, "display:none;" );
            atts.addAttribute( ID, toHtmlId( testCase.getFullName() ) + "-on" );
            sink.unknown( "span", TAG_TYPE_START, atts );
            sink.text( " - " );
            sink.unknown( "span", TAG_TYPE_END, null );

            sink.text( "[ Detail ]" );
            sinkLink_( sink );

            sink.unknown( "div", TAG_TYPE_END, null );

            sink.tableCell_();
        }
        else
        {
            sinkCellAnchor( sink, testCase.getName(), "TC_" + toHtmlId( testCase.getFullName() ) );
        }

        sinkCell( sink, numberFormat.format( testCase.getTime() ) );

        sink.tableRow_();

        if ( !testCase.isSuccessful() )
        {
            sink.tableRow();

            sinkCell( sink, "" );
            sinkCell( sink, testCase.getFailureMessage() );
            sinkCell( sink, "" );
            sink.tableRow_();

            String detail = testCase.getFailureDetail();
            if ( detail != null )
            {
                sink.tableRow();
                sinkCell( sink, "" );

                sink.tableCell();
                SinkEventAttributeSet atts = new SinkEventAttributeSet();
                atts.addAttribute( ID, toHtmlId( testCase.getFullName() ) + toHtmlIdFailure( testCase ) );
                atts.addAttribute( STYLE, "display:none;" );
                sink.unknown( "div", TAG_TYPE_START, atts );

                sink.verbatim( null );
                sink.text( detail );
                sink.verbatim_();

                sink.unknown( "div", TAG_TYPE_END, null );
                sink.tableCell_();

                sinkCell( sink, "" );

                sink.tableRow_();
            }
        }
    }

    private static String toHtmlId( String id )
    {
        return DoxiaUtils.isValidId( id ) ? id : DoxiaUtils.encodeId( id, true );
    }

    private void constructFailureDetails( Sink sink, LocalizedProperties bundle, List<ReportTestCase> failures )
    {
        sink.section1();
        sinkAnchor( sink, "Failure_Details" );
        sink.sectionTitle1();
        sink.text( bundle.getReportLabelFailureDetails() );
        sink.sectionTitle1_();

        constructHotLinks( sink, bundle );

        sinkLineBreak( sink );

        sink.table();

        sink.tableRows( new int[]{ LEFT, LEFT }, false );

        for ( ReportTestCase tCase : failures )
        {
            sink.tableRow();

            sink.tableCell();

            String type = tCase.getFailureType();

            sinkIcon( type, sink );

            sink.tableCell_();

            sinkCellAnchor( sink, tCase.getName(), toHtmlId( tCase.getFullName() ) );

            sink.tableRow_();

            String message = tCase.getFailureMessage();

            sink.tableRow();

            sinkCell( sink, "" );

            sinkCell( sink, message == null ? type : type + ": " + message );

            sink.tableRow_();

            String detail = tCase.getFailureDetail();
            if ( detail != null )
            {
                sink.tableRow();

                sinkCell( sink, "" );

                sink.tableCell();
                SinkEventAttributeSet atts = new SinkEventAttributeSet();
                atts.addAttribute( ID, tCase.getName() + toHtmlIdFailure( tCase ) );
                sink.unknown( "div", TAG_TYPE_START, atts );

                String fullClassName = tCase.getFullClassName();
                String errorLineNumber = tCase.getFailureErrorLine();
                if ( xrefLocation != null )
                {
                    String path = fullClassName.replace( '.', '/' );
                    sink.link( xrefLocation + "/" + path + ".html#L" + errorLineNumber );
                }
                sink.text( fullClassName + ":" + errorLineNumber );

                if ( xrefLocation != null )
                {
                    sink.link_();
                }
                sink.unknown( "div", TAG_TYPE_END, null );

                sink.tableCell_();

                sink.tableRow_();
            }
        }

        sink.tableRows_();

        sink.table_();

        sinkLineBreak( sink );

        sink.section1_();
    }

    private void constructHotLinks( Sink sink, LocalizedProperties bundle )
    {
        if ( !testSuites.isEmpty() )
        {
            sink.paragraph();

            sink.text( "[" );
            sinkLink( sink, bundle.getReportLabelSummary(), "#Summary" );
            sink.text( "]" );

            sink.text( " [" );
            sinkLink( sink, bundle.getReportLabelPackageList(), "#Package_List" );
            sink.text( "]" );

            sink.text( " [" );
            sinkLink( sink, bundle.getReportLabelTestCases(), "#Test_Cases" );
            sink.text( "]" );

            sink.paragraph_();
        }
    }

    private static String toHtmlIdFailure( ReportTestCase tCase )
    {
        return tCase.hasError() ? "-error" : "-failure";
    }

    private static void sinkLineBreak( Sink sink )
    {
        sink.lineBreak();
    }

    private static void sinkIcon( String type, Sink sink )
    {
        if ( type.startsWith( "junit.framework" ) || "skipped".equals( type ) )
        {
            sink.figureGraphics( "images/icon_warning_sml.gif" );
        }
        else if ( type.startsWith( "success" ) )
        {
            sink.figureGraphics( "images/icon_success_sml.gif" );
        }
        else
        {
            sink.figureGraphics( "images/icon_error_sml.gif" );
        }
    }

    private static void sinkHeader( Sink sink, String header )
    {
        sink.tableHeaderCell();
        sink.text( header );
        sink.tableHeaderCell_();
    }

    private static void sinkCell( Sink sink, String text )
    {
        sink.tableCell();
        sink.text( text );
        sink.tableCell_();
    }

    private static void sinkLink( Sink sink, String text, String link )
    {
        sink.link( link );
        sink.text( text );
        sink.link_();
    }

    private static void sinkCellLink( Sink sink, String text, String link )
    {
        sink.tableCell();
        sinkLink( sink, text, link );
        sink.tableCell_();
    }

    private static void sinkCellAnchor( Sink sink, String text, String anchor )
    {
        sink.tableCell();
        sinkAnchor( sink, anchor );
        sink.text( text );
        sink.tableCell_();
    }

    private static void sinkAnchor( Sink sink, String anchor )
    {
        // Dollar '$' for nested classes is not valid character in sink.anchor() and therefore it is ignored
        // https://issues.apache.org/jira/browse/SUREFIRE-1443
        sink.unknown( A.toString(), TAG_TYPE_START, new SinkEventAttributeSet( ID, anchor ) );
        sink.unknown( A.toString(), TAG_TYPE_END, null );
    }

    private static void sinkLink( Sink sink, String href )
    {
        // The "'" argument in this JavaScript function would be escaped to "&apos;"
        // sink.link( "javascript:toggleDisplay('" + toHtmlId( testCase.getFullName() ) + "');" );
        sink.unknown( A.toString(), TAG_TYPE_START, new SinkEventAttributeSet( HREF, href ) );
    }

    @SuppressWarnings( "checkstyle:methodname" )
    private static void sinkLink_( Sink sink )
    {
        sink.unknown( A.toString(), TAG_TYPE_END, null );
    }

    private static String javascriptToggleDisplayCode()
    {

        // the javascript code is emitted within a commented CDATA section
        // so we have to start with a newline and comment the CDATA closing in the end

        return "\n" + "function toggleDisplay(elementId) {\n"
                + " var elm = document.getElementById(elementId + '-error');\n"
                + " if (elm == null) {\n"
                + "  elm = document.getElementById(elementId + '-failure');\n"
                + " }\n"
                + " if (elm && typeof elm.style != \"undefined\") {\n"
                + "  if (elm.style.display == \"none\") {\n"
                + "   elm.style.display = \"\";\n"
                + "   document.getElementById(elementId + '-off').style.display = \"none\";\n"
                + "   document.getElementById(elementId + '-on').style.display = \"inline\";\n"
                + "  } else if (elm.style.display == \"\") {"
                + "   elm.style.display = \"none\";\n"
                + "   document.getElementById(elementId + '-off').style.display = \"inline\";\n"
                + "   document.getElementById(elementId + '-on').style.display = \"none\";\n"
                + "  } \n"
                + " } \n"
                + " }\n"
                + "//";
    }
}
