/*
 * Copyright (C) 2015 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.tradefed.result;

import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.invoker.IInvocationContext;

import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Result reporter to print the test results to the console.
 *
 * <p>Prints each test run, each test case, and test metrics, test logs, and test file locations.
 *
 * <p>
 */
@OptionClass(alias = "console-result-reporter")
public class ConsoleResultReporter extends TestResultListener
        implements ILogSaverListener, ITestInvocationListener {

    private static final SimpleDateFormat sTimeStampFormat = new SimpleDateFormat("HH:mm:ss");

    @Option(
            name = "suppress-passed-tests",
            description =
                    "For functional tests, ommit summary for "
                            + "passing tests, only print failed and ignored ones")
    private boolean mSuppressPassedTest = false;

    @Option(
            name = "display-failure-summary",
            description = "Display all the failures at the very end for easier visualization.")
    private boolean mDisplayFailureSummary = true;

    @Option(
            name = "display-invocation-attributes",
            description =
                    "Display all the invocation attributes at the very end for easier"
                            + " visualization.")
    private boolean mDisplayInvocationAttributes = false;

    private final PrintStream mStream;
    private Set<LogFile> mLoggedFiles = new LinkedHashSet<>();
    private Map<TestDescription, TestResult> mFailures = new LinkedHashMap<>();
    private String mTestTag;
    private String mRunInProgress;
    private CountingTestResultListener mResultCountListener = new CountingTestResultListener();
    private IInvocationContext mContext;

    public ConsoleResultReporter() {
        this(System.out);
    }

    ConsoleResultReporter(PrintStream outputStream) {
        mStream = outputStream;
    }

    @Override
    public void invocationStarted(IInvocationContext context) {
        mTestTag = context.getTestTag();
        mContext = context;
    }

    @Override
    public void testResult(TestDescription test, TestResult result) {
        mResultCountListener.testResult(test, result);
        if (mSuppressPassedTest && TestStatus.PASSED.equals(result.getResultStatus())) {
            return;
        }
        if (mDisplayFailureSummary && TestStatus.FAILURE.equals(result.getResultStatus())) {
            mFailures.put(test, result);
        }
        print(getTestSummary(mTestTag, test, result));
    }

    @Override
    public void testRunStarted(String runName, int testCount) {
        super.testRunStarted(runName, testCount);
        mRunInProgress = runName;
    }

    @Override
    public void testRunFailed(String errorMessage) {
        print(String.format("%s: run failed: %s\n", mRunInProgress, errorMessage));
    }

    @Override
    public void testRunFailed(FailureDescription failure) {
        print(String.format("%s: run failed: %s\n", mRunInProgress, failure));
    }

    @Override
    public void testRunEnded(long elapsedTimeMillis, Map<String, String> metrics) {
        super.testRunEnded(elapsedTimeMillis, metrics);
        if (metrics != null && !metrics.isEmpty()) {
            String tag = mTestTag != null ? mTestTag : "unknown";
            String runName = mRunInProgress != null ? mRunInProgress : "unknown";
            StringBuilder sb = new StringBuilder(tag);
            sb.append(": ");
            sb.append(runName);
            sb.append(": ");
            List<String> metricKeys = new ArrayList<String>(metrics.keySet());
            Collections.sort(metricKeys);
            for (String metricKey : metricKeys) {
                sb.append(String.format("%s=%s\n", metricKey, metrics.get(metricKey)));
            }
            print(sb.toString());
        }
        mRunInProgress = null;
    }

    /** {@inheritDoc} */
    @Override
    public void invocationEnded(long elapsedTime) {
        int[] results = mResultCountListener.getResultCounts();
        StringBuilder sb = new StringBuilder();
        sb.append("========== Result Summary ==========");
        sb.append(String.format("\nResults summary for test-tag '%s': ", mTestTag));
        sb.append(mResultCountListener.getTotalTests());
        sb.append(" Tests [");
        sb.append(results[TestStatus.PASSED.ordinal()]);
        sb.append(" Passed");
        if (results[TestStatus.FAILURE.ordinal()] > 0) {
            sb.append(" ");
            sb.append(results[TestStatus.FAILURE.ordinal()]);
            sb.append(" Failed");
        }
        if (results[TestStatus.IGNORED.ordinal()] > 0) {
            sb.append(" ");
            sb.append(results[TestStatus.IGNORED.ordinal()]);
            sb.append(" Ignored");
        }
        if (results[TestStatus.SKIPPED.ordinal()] > 0) {
            sb.append(" ");
            sb.append(results[TestStatus.SKIPPED.ordinal()]);
            sb.append(" Skipped");
        }
        if (results[TestStatus.ASSUMPTION_FAILURE.ordinal()] > 0) {
            sb.append(" ");
            sb.append(results[TestStatus.ASSUMPTION_FAILURE.ordinal()]);
            sb.append(" Assumption failures");
        }
        if (results[TestStatus.INCOMPLETE.ordinal()] > 0) {
            sb.append(" ");
            sb.append(results[TestStatus.INCOMPLETE.ordinal()]);
            sb.append(" Incomplete");
        }
        sb.append("] \r\n");
        print(sb.toString());
        if (mDisplayInvocationAttributes && !mContext.getAttributes().isEmpty()) {
            StringBuilder metricPrint = new StringBuilder();
            metricPrint.append(" Metrics:\n");
            for (String key : mContext.getAttributes().keySet()) {
                metricPrint.append(
                        "   " + key + "=" + mContext.getAttributes().get(key).toString() + "\n");
            }
            print(metricPrint.toString());
        }
        if (mDisplayFailureSummary) {
            for (Entry<TestDescription, TestResult> entry : mFailures.entrySet()) {
                print(getTestSummary(mTestTag, entry.getKey(), entry.getValue()));
            }
        }
        // Print the logs
        for (LogFile logFile : mLoggedFiles) {
            printLog(logFile);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void logAssociation(String dataName, LogFile logFile) {
        mLoggedFiles.add(logFile);
    }

    /** {@inheritDoc} */
    @Override
    public void testLogSaved(
            String dataName, LogDataType dataType, InputStreamSource dataStream, LogFile logFile) {
        mLoggedFiles.add(logFile);
    }

    private void printLog(LogFile logFile) {
        if (mSuppressPassedTest && !mResultCountListener.hasFailedTests()) {
            // all tests passed, skip logging
            return;
        }
        String logDesc = logFile.getUrl() == null ? logFile.getPath() : logFile.getUrl();
        print("Log: " + logDesc + "\r\n");
    }

    /** Get the test summary as string including test metrics. */
    static String getTestSummary(String testTag, TestDescription testId, TestResult testResult) {
        StringBuilder sb = new StringBuilder();
        sb.append(
                String.format(
                        "%s: %s: %s (%dms)\n",
                        testTag,
                        testId.toString(),
                        testResult.getStatus(),
                        testResult.getEndTime() - testResult.getStartTime()));
        String stack = testResult.getStackTrace();
        if (stack != null && !stack.isEmpty()) {
            sb.append("  stack=\n");
            String lines[] = stack.split("\\r?\\n");
            for (String line : lines) {
                sb.append(String.format("    %s\n", line));
            }
        }
        Map<String, String> metrics = testResult.getMetrics();
        if (metrics != null && !metrics.isEmpty()) {
            List<String> metricKeys = new ArrayList<String>(metrics.keySet());
            Collections.sort(metricKeys);
            for (String metricKey : metricKeys) {
                sb.append(String.format("    %s: %s\n", metricKey, metrics.get(metricKey)));
            }
        }

        return sb.toString();
    }

    private void print(String msg) {
        mStream.print(sTimeStampFormat.format(new Date()) + " " + msg);
    }
}
