/*
 * 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.
 */
package org.apache.maven.plugin.surefire.report;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.apache.maven.plugin.surefire.runorder.StatisticsReporter;
import org.apache.maven.surefire.api.report.ReportEntry;
import org.apache.maven.surefire.api.report.TestOutputReportEntry;
import org.apache.maven.surefire.api.report.TestReportListener;
import org.apache.maven.surefire.api.report.TestSetReportEntry;
import org.apache.maven.surefire.extensions.ConsoleOutputReportEventListener;
import org.apache.maven.surefire.extensions.StatelessReportEventListener;
import org.apache.maven.surefire.extensions.StatelessTestsetInfoConsoleReportEventListener;
import org.apache.maven.surefire.extensions.StatelessTestsetInfoFileReportEventListener;

import static org.apache.maven.plugin.surefire.report.ReportEntryType.ERROR;
import static org.apache.maven.plugin.surefire.report.ReportEntryType.FAILURE;
import static org.apache.maven.plugin.surefire.report.ReportEntryType.SKIPPED;
import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;

/**
 * Reports data for a single test set.
 * <br>
 *
 * @author Kristian Rosenvold
 */
public class TestSetRunListener implements TestReportListener<TestOutputReportEntry> {
    private final Queue<TestMethodStats> testMethodStats = new ConcurrentLinkedQueue<>();

    private final TestSetStats detailsForThis;

    private final ConsoleOutputReportEventListener testOutputReceiver;

    private final boolean briefOrPlainFormat;

    private final StatelessReportEventListener<WrappedReportEntry, TestSetStats> simpleXMLReporter;

    private final StatelessTestsetInfoConsoleReportEventListener<WrappedReportEntry, TestSetStats> consoleReporter;

    private final StatelessTestsetInfoFileReportEventListener<WrappedReportEntry, TestSetStats> fileReporter;

    private final StatisticsReporter statisticsReporter;

    private final Object lock;

    private Utf8RecodingDeferredFileOutputStream testStdOut = initDeferred("stdout");

    private Utf8RecodingDeferredFileOutputStream testStdErr = initDeferred("stderr");

    @SuppressWarnings("checkstyle:parameternumber")
    public TestSetRunListener(
            StatelessTestsetInfoConsoleReportEventListener<WrappedReportEntry, TestSetStats> consoleReporter,
            StatelessTestsetInfoFileReportEventListener<WrappedReportEntry, TestSetStats> fileReporter,
            StatelessReportEventListener<WrappedReportEntry, TestSetStats> simpleXMLReporter,
            ConsoleOutputReportEventListener testOutputReceiver,
            StatisticsReporter statisticsReporter,
            boolean trimStackTrace,
            boolean isPlainFormat,
            boolean briefOrPlainFormat,
            Object lock) {
        this.consoleReporter = consoleReporter;
        this.fileReporter = fileReporter;
        this.statisticsReporter = statisticsReporter;
        this.simpleXMLReporter = simpleXMLReporter;
        this.testOutputReceiver = testOutputReceiver;
        this.briefOrPlainFormat = briefOrPlainFormat;
        detailsForThis = new TestSetStats(trimStackTrace, isPlainFormat);
        this.lock = lock;
    }

    @Override
    public boolean isDebugEnabled() {
        return consoleReporter.getConsoleLogger().isDebugEnabled();
    }

    @Override
    public void debug(String message) {
        synchronized (lock) {
            consoleReporter.getConsoleLogger().debug(trimTrailingNewLine(message));
        }
    }

    @Override
    public boolean isInfoEnabled() {
        return consoleReporter.getConsoleLogger().isInfoEnabled();
    }

    @Override
    public void info(String message) {
        synchronized (lock) {
            consoleReporter.getConsoleLogger().info(trimTrailingNewLine(message));
        }
    }

    @Override
    public boolean isWarnEnabled() {
        return consoleReporter.getConsoleLogger().isWarnEnabled();
    }

    @Override
    public void warning(String message) {
        synchronized (lock) {
            consoleReporter.getConsoleLogger().warning(trimTrailingNewLine(message));
        }
    }

    @Override
    public boolean isErrorEnabled() {
        return consoleReporter.getConsoleLogger().isErrorEnabled();
    }

    @Override
    public void error(String message) {
        synchronized (lock) {
            consoleReporter.getConsoleLogger().error(trimTrailingNewLine(message));
        }
    }

    @Override
    public void error(String message, Throwable t) {
        synchronized (lock) {
            consoleReporter.getConsoleLogger().error(trimTrailingNewLine(message), t);
        }
    }

    @Override
    public void error(Throwable t) {
        synchronized (lock) {
            consoleReporter.getConsoleLogger().error(t);
        }
    }

    @Override
    public void writeTestOutput(TestOutputReportEntry reportEntry) {
        try {
            synchronized (lock) {
                Utf8RecodingDeferredFileOutputStream stream = reportEntry.isStdOut() ? testStdOut : testStdErr;
                stream.write(reportEntry.getLog(), reportEntry.isNewLine());
                testOutputReceiver.writeTestOutput(reportEntry);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void testSetStarting(TestSetReportEntry report) {
        detailsForThis.testSetStart();
        consoleReporter.testSetStarting(report);
        testOutputReceiver.testSetStarting(report);
    }

    private void clearCapture() {
        if (testStdOut != null) {
            testStdOut.commit();
        }

        if (testStdErr != null) {
            testStdErr.commit();
        }

        testStdOut = initDeferred("stdout");
        testStdErr = initDeferred("stderr");
    }

    @Override
    public void testSetCompleted(TestSetReportEntry report) {
        final WrappedReportEntry wrap = wrapTestSet(report);
        final List<String> testResults =
                briefOrPlainFormat ? detailsForThis.getTestResults() : Collections.<String>emptyList();
        fileReporter.testSetCompleted(wrap, detailsForThis, testResults);
        simpleXMLReporter.testSetCompleted(wrap, detailsForThis);
        statisticsReporter.testSetCompleted();
        consoleReporter.testSetCompleted(wrap, detailsForThis, testResults);
        testOutputReceiver.testSetCompleted(wrap);
        consoleReporter.reset();

        wrap.getStdout().free();
        wrap.getStdErr().free();

        addTestMethodStats();
        detailsForThis.reset();
        clearCapture();
    }

    // ----------------------------------------------------------------------
    // Test callback methods:
    // ----------------------------------------------------------------------

    @Override
    public void testStarting(ReportEntry report) {
        detailsForThis.testStart();
    }

    @Override
    public void testSucceeded(ReportEntry reportEntry) {
        WrappedReportEntry wrapped = wrap(reportEntry, SUCCESS);
        detailsForThis.testSucceeded(wrapped);
        statisticsReporter.testSucceeded(reportEntry);
        clearCapture();
    }

    @Override
    public void testError(ReportEntry reportEntry) {
        WrappedReportEntry wrapped = wrap(reportEntry, ERROR);
        detailsForThis.testError(wrapped);
        statisticsReporter.testError(reportEntry);
        clearCapture();
    }

    @Override
    public void testFailed(ReportEntry reportEntry) {
        WrappedReportEntry wrapped = wrap(reportEntry, FAILURE);
        detailsForThis.testFailure(wrapped);
        statisticsReporter.testFailed(reportEntry);
        clearCapture();
    }

    // ----------------------------------------------------------------------
    // Counters
    // ----------------------------------------------------------------------

    @Override
    public void testSkipped(ReportEntry reportEntry) {
        WrappedReportEntry wrapped = wrap(reportEntry, SKIPPED);
        detailsForThis.testSkipped(wrapped);
        statisticsReporter.testSkipped(reportEntry);
        clearCapture();
    }

    @Override
    public void testExecutionSkippedByUser() {
        clearCapture();
    }

    @Override
    public void testAssumptionFailure(ReportEntry report) {
        testSkipped(report);
    }

    private WrappedReportEntry wrap(ReportEntry other, ReportEntryType reportEntryType) {
        int estimatedElapsed = 0;
        if (reportEntryType != SKIPPED) {
            Integer etime = other.getElapsed();
            estimatedElapsed = etime == null ? detailsForThis.getElapsedSinceLastStart() : etime;
        }

        return new WrappedReportEntry(other, reportEntryType, estimatedElapsed, testStdOut, testStdErr);
    }

    private WrappedReportEntry wrapTestSet(TestSetReportEntry other) {
        return new WrappedReportEntry(
                other,
                null,
                other.getElapsed() != null ? other.getElapsed() : detailsForThis.getElapsedSinceTestSetStart(),
                testStdOut,
                testStdErr,
                other.getSystemProperties());
    }

    public void close() {
        testOutputReceiver.close();
    }

    private void addTestMethodStats() {
        for (WrappedReportEntry reportEntry : detailsForThis.getReportEntries()) {
            TestMethodStats methodStats = new TestMethodStats(
                    reportEntry.getClassMethodName(),
                    reportEntry.getReportEntryType(),
                    reportEntry.getStackTraceWriter());
            testMethodStats.add(methodStats);
        }
    }

    public Queue<TestMethodStats> getTestMethodStats() {
        return testMethodStats;
    }

    private static String trimTrailingNewLine(final String message) {
        final int e = message == null ? 0 : lineBoundSymbolWidth(message);
        return message != null && e != 0 ? message.substring(0, message.length() - e) : message;
    }

    private static int lineBoundSymbolWidth(String message) {
        return message.endsWith("\r\n") ? 2 : (message.endsWith("\n") || message.endsWith("\r") ? 1 : 0);
    }

    private static Utf8RecodingDeferredFileOutputStream initDeferred(String channel) {
        return new Utf8RecodingDeferredFileOutputStream(channel);
    }
}
