/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/

#include "boosttestoutputreader.h"
#include "boosttestsettings.h"
#include "boosttestresult.h"

#include <utils/qtcassert.h>

#include <QDir>
#include <QFileInfo>
#include <QLoggingCategory>
#include <QRegularExpression>

namespace Autotest {
namespace Internal {

static Q_LOGGING_CATEGORY(orLog, "qtc.autotest.boost.outputreader", QtWarningMsg)

static QString constructSourceFilePath(const QString &path, const QString &filePath)
{
    return QFileInfo(path, filePath).canonicalFilePath();
}

BoostTestOutputReader::BoostTestOutputReader(const QFutureInterface<TestResultPtr> &futureInterface,
                                             QProcess *testApplication,
                                             const QString &buildDirectory,
                                             const QString &projectFile,
                                             LogLevel log, ReportLevel report)
    : TestOutputReader(futureInterface, testApplication, buildDirectory)
    , m_projectFile(projectFile)
    , m_logLevel(log)
    , m_reportLevel(report)
{
    if (m_testApplication) {
        connect(m_testApplication, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
                this, &BoostTestOutputReader::onFinished);
    }
}

// content of "error:..." / "info:..." / ... messages
static QString caseFromContent(const QString &content)
{
    const int length = content.length();
    if (content.startsWith("last checkpoint:")) {
        int index = content.indexOf('"');
        if (index != 17 || length <= 18) {
            qCDebug(orLog) << "double quote position" << index << " or content length" << length
                           << "wrong on content" << content;
            return QString();
        }
        index = content.indexOf('"', 18);
        if (index == -1) {
            qCDebug(orLog) << "no closing double quote" << content;
            return QString();
        }
        return content.mid(18, index - 1);
    }

    int index = content.indexOf(": in ");
    if (index == -1) // "info: check true has passed"
        return QString();

    if (index <= 4 || length < index + 4) {
        qCDebug(orLog) << "unexpected position" << index << "for info" << content;
        return QString();
    }

    QString result = content.mid(index + 5);
    static QRegularExpression functionName("\"(.+)\":.*");
    const QRegularExpressionMatch matcher = functionName.match(result);
    if (!matcher.hasMatch()) {
        qCDebug(orLog) << "got no match";
        return QString();
    }
    return matcher.captured(1);
}

void BoostTestOutputReader::sendCompleteInformation()
{
    QTC_ASSERT(m_result != ResultType::Invalid, return);
    BoostTestResult *result = new BoostTestResult(id(), m_projectFile, m_currentModule);
    result->setTestSuite(m_currentSuite);
    result->setTestCase(m_currentTest);
    if (m_lineNumber) {
        result->setLine(m_lineNumber);
        result->setFileName(m_fileName);
    }

    result->setDescription(m_description);
    result->setResult(m_result);
    reportResult(TestResultPtr(result));
    m_result = ResultType::Invalid;
}

void BoostTestOutputReader::handleMessageMatch(const QRegularExpressionMatch &match)
{
    m_fileName = constructSourceFilePath(m_buildDir, match.captured(1));
    m_lineNumber = match.captured(2).toInt();

    const QString &content = match.captured(3);
    if (content.startsWith("info:")) {
        if (m_currentTest.isEmpty() || m_logLevel > LogLevel::UnitScope) {
            QString tmp = caseFromContent(content);
            if (!tmp.isEmpty())
                m_currentTest = tmp;
        }
        m_result = ResultType::Pass;
        m_description = content;
    } else if (content.startsWith("error:")) {
        if (m_currentTest.isEmpty() || m_logLevel > LogLevel::UnitScope)
            m_currentTest = caseFromContent(content);
        m_result = ResultType::Fail;
        if (m_reportLevel == ReportLevel::No)
            ++m_summary[ResultType::Fail];
        m_description = content;
    } else if (content.startsWith("fatal error:")) {
        if (m_currentTest.isEmpty() || m_logLevel > LogLevel::UnitScope)
            m_currentTest = caseFromContent(content);
        m_result = ResultType::MessageFatal;
        m_description = content;
    } else if (content.startsWith("last checkpoint:")) {
        if (m_currentTest.isEmpty() || m_logLevel > LogLevel::UnitScope)
            m_currentTest = caseFromContent(content);
        m_result = ResultType::MessageInfo;
        m_description = content;
    } else if (content.startsWith("Entering")) {
        m_result = ResultType::TestStart;
        const QString type = match.captured(8);
        if (type == "case") {
            m_currentTest = match.captured(9);
            m_description = tr("Executing test case %1").arg(m_currentTest);
        } else if (type == "suite") {
            m_currentSuite = match.captured(9);
            m_description = tr("Executing test suite %1").arg(m_currentSuite);
        }
    } else if (content.startsWith("Leaving")) {
        const QString type = match.captured(10);
        if (type == "case") {
            if (m_currentTest != match.captured(11) && m_currentTest.isEmpty())
                m_currentTest = match.captured(11);
            m_result = ResultType::TestEnd;
            m_description = tr("Test execution took %1").arg(match.captured(12));
        } else if (type == "suite") {
            if (m_currentSuite != match.captured(11) && m_currentSuite.isEmpty())
                m_currentSuite = match.captured(11);
            m_currentTest.clear();
            m_result = ResultType::TestEnd;
            m_description = tr("Test suite execution took %1").arg(match.captured(12));
        }
    } else if (content.startsWith("Test case ")) {
        m_currentTest = match.captured(4);
        m_result = ResultType::Skip;
        if (m_reportLevel == ReportLevel::Confirm || m_reportLevel == ReportLevel::No)
            ++m_summary[ResultType::Skip];
        m_description = content;
    }

    if (m_result != ResultType::Invalid) // we got a new result
        sendCompleteInformation();
}

void BoostTestOutputReader::processOutputLine(const QByteArray &outputLineWithNewLine)
{
    static QRegularExpression newTestStart("^Running (\\d+) test cases?\\.\\.\\.$");
    static QRegularExpression dependency("^Including test case (.+) as a dependency of "
                                         "test case (.+)$");
    static QRegularExpression messages("^(.+)\\((\\d+)\\): (info: (.+)|error: (.+)|"
                                       "fatal error: (.+)|last checkpoint: (.+)"
                                       "|Entering test (case|suite) \"(.+)\""
                                       "|Leaving test (case|suite) \"(.+)\"; testing time: (\\d+.+)"
                                       "|Test case \"(.+)\" is skipped because .+$)$");
    static QRegularExpression moduleMssg("^(Entering test module \"(.+)\"|"
                                         "Leaving test module \"(.+)\"; testing time: (\\d+.+))$");
    static QRegularExpression noAssertion("^Test case (.*) did not check any assertions$");

    static QRegularExpression summaryPreamble("^\\s*Test (module|suite|case) \"(.*)\" has "
                                              "(failed|passed)( with:)?$");
    static QRegularExpression summarySkip("^\\s+Test case \"(.*)\" was skipped$");
    static QRegularExpression summaryDetail("^\\s+(\\d+) test cases? out of (\\d+) "
                                            "(failed|passed|skipped)$");
    static QRegularExpression summaryAssertion("^\\s+(\\d+) assertions? out of (\\d+) "
                                               "(failed|passed)$");

    static QRegularExpression finish("^\\*{3} (\\d+) failure(s are| is) detected in the "
                                     "test module \"(.*)\"$");
    static QRegularExpression errDetect("^\\*{3} Errors where detected in the "
                                        "test module \"(.*}\"; see standard output for details");
    QString noErrors("*** No errors detected");

    const QString line = QString::fromUtf8(chopLineBreak(outputLineWithNewLine));
    if (line.trimmed().isEmpty())
        return;

    QRegularExpressionMatch match = messages.match(line);
    if (match.hasMatch()) {
        handleMessageMatch(match);
        return;
    }

    match = dependency.match(line);
    if (match.hasMatch()) {
        if (m_result != ResultType::Invalid)
            sendCompleteInformation();
        BoostTestResult *result = new BoostTestResult(id(), m_projectFile, m_currentModule);
        result->setDescription(match.captured(0));
        result->setResult(ResultType::MessageInfo);
        reportResult(TestResultPtr(result));
        return;
    }

    match = newTestStart.match(line);
    if (match.hasMatch()) {
        if (m_result != ResultType::Invalid)
            sendCompleteInformation();
        m_testCaseCount = match.captured(1).toInt();
        m_description.clear();
        return;
    }

    match = moduleMssg.match(line);
    if (match.hasMatch()) {
        if (m_result != ResultType::Invalid)
            sendCompleteInformation();
        if (match.captured(1).startsWith("Entering")) {
            m_currentModule = match.captured(2);
            BoostTestResult *result = new BoostTestResult(id(), m_projectFile, m_currentModule);
            result->setDescription(tr("Executing test module %1").arg(m_currentModule));
            result->setResult(ResultType::TestStart);
            reportResult(TestResultPtr(result));
            m_description.clear();
        } else {
            QTC_CHECK(m_currentModule == match.captured(3));
            BoostTestResult *result = new BoostTestResult(id(), m_projectFile, m_currentModule);
            result->setDescription(tr("Test module execution took %1").arg(match.captured(4)));
            result->setResult(ResultType::TestEnd);
            reportResult(TestResultPtr(result));

            m_currentTest.clear();
            m_currentSuite.clear();
            m_currentModule.clear();
            m_description.clear();
        }
        return;
    }

    match = noAssertion.match(line);
    if (match.hasMatch()) {
        if (m_result != ResultType::Invalid)
            sendCompleteInformation();
        const QString caseWithOptionalSuite = match.captured(1);
        int index = caseWithOptionalSuite.lastIndexOf('/');
        if (index == -1) {
            QTC_CHECK(caseWithOptionalSuite == m_currentTest);
        } else {
            QTC_CHECK(caseWithOptionalSuite.mid(index + 1) == m_currentTest);
            int sIndex = caseWithOptionalSuite.lastIndexOf('/', index - 1);
            if (sIndex == -1) {
                QTC_CHECK(caseWithOptionalSuite.left(index) == m_currentSuite);
                m_currentSuite = caseWithOptionalSuite.left(index); // FIXME should not be necessary - but we currently do not care for the whole suite path
            } else {
                QTC_CHECK(caseWithOptionalSuite.mid(sIndex + 1, index - sIndex - 1) == m_currentSuite);
            }
        }
        createAndReportResult(match.captured(0), ResultType::MessageWarn);
        return;
    }

    match = summaryPreamble.match(line);
    if (match.hasMatch()) {
        createAndReportResult(match.captured(0), ResultType::MessageInfo);
        if (m_reportLevel == ReportLevel::Detailed || match.captured(4).isEmpty()) {
            if (match.captured(1) == "case") {
                if (match.captured(3) == "passed")
                    ++m_summary[ResultType::Pass];
                else
                    ++m_summary[ResultType::Fail];
            }
        }
        return;
    }

    match = summaryDetail.match(line);
    if (match.hasMatch()) {
        createAndReportResult(match.captured(0), ResultType::MessageInfo);
        int report = match.captured(1).toInt();
        QString type = match.captured(3);
        if (m_reportLevel != ReportLevel::Detailed) {
            if (type == "passed")
                m_summary[ResultType::Pass] += report;
            else if (type == "failed")
                m_summary[ResultType::Fail] += report;
            else if (type == "skipped")
                m_summary[ResultType::Skip] += report;
        }

        return;
    }

    match = summaryAssertion.match(line);
    if (match.hasMatch()) {
        createAndReportResult(match.captured(0), ResultType::MessageInfo);
        return;
    }

    match = summarySkip.match(line);
    if (match.hasMatch()) {
        createAndReportResult(match.captured(0), ResultType::MessageInfo);
        if (m_reportLevel == ReportLevel::Detailed)
            ++m_summary[ResultType::Skip];
        return;
    }

    match = finish.match(line);
    if (match.hasMatch()) {
        if (m_result != ResultType::Invalid)
            sendCompleteInformation();
        BoostTestResult *result = new BoostTestResult(id(), m_projectFile, QString());
        int failed = match.captured(1).toInt();
        QString txt = tr("%1 failures detected in %2.").arg(failed).arg(match.captured(3));
        int passed = (m_testCaseCount != -1)
                ? m_testCaseCount - failed - m_summary[ResultType::Skip] : -1;
        if (m_testCaseCount != -1)
            txt.append(' ').append(tr("%1 tests passed.").arg(passed));
        result->setDescription(txt);
        result->setResult(ResultType::MessageInfo);
        reportResult(TestResultPtr(result));
        if (m_reportLevel == ReportLevel::Confirm) { // for the final summary
            m_summary[ResultType::Pass] += passed;
            m_summary[ResultType::Fail] += failed;
        }
        m_testCaseCount = -1;
        return;
    }

    if (line == noErrors) {
        if (m_result != ResultType::Invalid)
            sendCompleteInformation();
        BoostTestResult *result = new BoostTestResult(id(), m_projectFile, QString());
        QString txt = tr("No errors detected.");
        if (m_testCaseCount != -1)
            txt.append(' ').append(tr("%1 tests passed.").arg(m_testCaseCount));
        result->setDescription(txt);
        result->setResult(ResultType::MessageInfo);
        reportResult(TestResultPtr(result));
        if (m_reportLevel == ReportLevel::Confirm) // for the final summary
            m_summary.insert(ResultType::Pass, m_testCaseCount);
        return;
    }

    // some plain output...
    if (!m_description.isEmpty())
        m_description.append('\n');
    m_description.append(line);
}

void BoostTestOutputReader::processStdError(const QByteArray &output)
{
    // we need to process the output, Boost UTF uses both out streams
    int start = 0;
    int index = -1;
    while ((index = output.indexOf('\n', start)) != -1) {
        const QByteArray &line = output.mid(start, index - start + 1);
        if (!line.isEmpty()) {
            if (line != QByteArray(1, '\n'))
                processOutputLine(line);
            emit newOutputAvailable(line);
        }
        start = index + 1;
    }
    if (start > 0) { // remove? this never happens
        const QByteArray lastLine = output.mid(start) + '\n';
        if (!lastLine.isEmpty()) {
            if (lastLine != QByteArray(1, '\n'))
                processOutputLine(lastLine);
            emit newOutputAvailable(lastLine);
        }
    }
}

TestResultPtr BoostTestOutputReader::createDefaultResult() const
{
    BoostTestResult *result = new BoostTestResult(id(), m_projectFile, m_currentModule);
    result->setTestSuite(m_currentSuite);
    result->setTestCase(m_currentTest);

    return TestResultPtr(result);
}

void BoostTestOutputReader::onFinished(int exitCode, QProcess::ExitStatus /*exitState*/) {
    if (m_reportLevel == ReportLevel::No && m_testCaseCount != -1) {
        int reportedFailsAndSkips = m_summary[ResultType::Fail] + m_summary[ResultType::Skip];
        m_summary.insert(ResultType::Pass, m_testCaseCount - reportedFailsAndSkips);
    }
    // boost::exit_success (0), boost::exit_test_failure (201)
    // or boost::exit_exception_failure (200)
    // be graceful and do not add a fatal for exit_test_failure
    if (m_logLevel == LogLevel::Nothing && m_reportLevel == ReportLevel::No) {
        switch (exitCode) {
        case 0:
            reportNoOutputFinish(tr("Running tests exited with ") + "boost::exit_success.",
                                 ResultType::Pass);
            break;
        case 200:
            reportNoOutputFinish(
                        tr("Running tests exited with ") + "boost::exit_test_exception.",
                        ResultType::MessageFatal);
            break;
        case 201:
            reportNoOutputFinish(tr("Running tests exited with ")
                                 + "boost::exit_test_failure.", ResultType::Fail);
            break;
        }
    } else if (exitCode != 0 && exitCode != 201 && !m_description.isEmpty()) {
        if (m_description.startsWith("Test setup error:")) {
            createAndReportResult(m_description + '\n' + tr("Executable: %1")
                                  .arg(id()), ResultType::MessageWarn);
        } else {
            createAndReportResult(tr("Running tests failed.\n%1\nExecutable: %2")
                                  .arg(m_description).arg(id()), ResultType::MessageFatal);
        }
    }
}

void BoostTestOutputReader::reportNoOutputFinish(const QString &description, ResultType type)
{
    BoostTestResult *result = new BoostTestResult(id(), m_projectFile, m_currentModule);
    result->setTestCase(tr("Running tests without output."));
    result->setDescription(description);
    result->setResult(type);
    reportResult(TestResultPtr(result));
}

} // namespace Internal
} // namespace Autotest
