// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include <QtTest/private/qtestresult_p.h>
#include <QtTest/qtestassert.h>
#include <QtTest/private/qtestlog_p.h>
#include <QtTest/private/qplaintestlogger_p.h>
#include <QtTest/private/qbenchmark_p.h>
#include <QtTest/private/qbenchmarkmetric_p.h>

#include <QtCore/private/qlogging_p.h>

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef min // windows.h without NOMINMAX is included by the benchmark headers.
#  undef min
#endif
#ifdef max
#  undef max
#endif

#include <QtCore/QByteArray>
#include <QtCore/qmath.h>
#include <QtCore/QLibraryInfo>

#ifdef Q_OS_ANDROID
#  include <android/log.h>
#endif

#ifdef Q_OS_WIN
#  include <qt_windows.h>
#endif

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

namespace QTest {

    static const char *incidentType2String(QAbstractTestLogger::IncidentTypes type)
    {
        switch (type) {
        case QAbstractTestLogger::Skip:
            return "SKIP   ";
        case QAbstractTestLogger::Pass:
            return "PASS   ";
        case QAbstractTestLogger::XFail:
            return "XFAIL  ";
        case QAbstractTestLogger::Fail:
            return "FAIL!  ";
        case QAbstractTestLogger::XPass:
            return "XPASS  ";
        case QAbstractTestLogger::BlacklistedPass:
            return "BPASS  ";
        case QAbstractTestLogger::BlacklistedFail:
            return "BFAIL  ";
        case QAbstractTestLogger::BlacklistedXPass:
            return "BXPASS ";
        case QAbstractTestLogger::BlacklistedXFail:
            return "BXFAIL ";
        }
        Q_UNREACHABLE();
        return nullptr;
    }

    static const char *benchmarkResult2String()
    {
        return "RESULT ";
    }

    static const char *messageType2String(QAbstractTestLogger::MessageTypes type)
    {
        switch (type) {
        case QAbstractTestLogger::QDebug:
            return "QDEBUG ";
        case QAbstractTestLogger::QInfo:
            return "QINFO  ";
        case QAbstractTestLogger::QWarning:
            return "QWARN  ";
        case QAbstractTestLogger::QCritical:
            return "QCRITICAL";
        case QAbstractTestLogger::QFatal:
            return "QFATAL ";
        case QAbstractTestLogger::Info:
            return "INFO   ";
        case QAbstractTestLogger::Warn:
            return "WARNING";
        }
        Q_UNREACHABLE();
        return nullptr;
    }

    template <typename T>
    static int countSignificantDigits(T num)
    {
        if (num <= 0)
            return 0;

        int digits = 0;
        qreal divisor = 1;

        while (num / divisor >= 1) {
            divisor *= 10;
            ++digits;
        }

        return digits;
    }

    // Pretty-prints a benchmark result using the given number of digits.
    template <typename T> QString formatResult(T number, int significantDigits)
    {
        if (number < T(0))
            return u"NAN"_s;
        if (number == T(0))
            return u"0"_s;

        QString beforeDecimalPoint = QString::number(qint64(number), 'f', 0);
        QString afterDecimalPoint = QString::number(number, 'f', 20);
        afterDecimalPoint.remove(0, beforeDecimalPoint.size() + 1);

        int beforeUse = qMin(beforeDecimalPoint.size(), significantDigits);
        int beforeRemove = beforeDecimalPoint.size() - beforeUse;

        // Replace insignificant digits before the decimal point with zeros.
        beforeDecimalPoint.chop(beforeRemove);
        for (int i = 0; i < beforeRemove; ++i) {
            beforeDecimalPoint.append(u'0');
        }

        int afterUse = significantDigits - beforeUse;

        // leading zeroes after the decimal point does not count towards the digit use.
        if (beforeDecimalPoint == u'0' && !afterDecimalPoint.isEmpty()) {
            ++afterUse;

            int i = 0;
            while (i < afterDecimalPoint.size() && afterDecimalPoint.at(i) == u'0')
                ++i;

            afterUse += i;
        }

        int afterRemove = afterDecimalPoint.size() - afterUse;
        afterDecimalPoint.chop(afterRemove);

        QChar separator = u',';
        QChar decimalPoint = u'.';

        // insert thousands separators
        int length = beforeDecimalPoint.size();
        for (int i = beforeDecimalPoint.size() -1; i >= 1; --i) {
            if ((length - i) % 3 == 0)
                beforeDecimalPoint.insert(i, separator);
        }

        QString print;
        print = beforeDecimalPoint;
        if (afterUse > 0)
            print.append(decimalPoint);

        print += afterDecimalPoint;


        return print;
    }

    template <typename T>
    int formatResult(char * buffer, int bufferSize, T number, int significantDigits)
    {
        QString result = formatResult(number, significantDigits);
        int size = result.size();
        qstrncpy(buffer, std::move(result).toLatin1().constData(), bufferSize);
        return size;
    }
}

/*! \internal
    \class QPlainTestLogger
    \inmodule QtTest

    QPlainTestLogger implements basic logging of test results.

    The format is Qt-specific and aims to be be easy to read.
*/

void QPlainTestLogger::outputMessage(const char *str)
{
#if defined(Q_OS_WIN)
    // Log to system log only if output is not redirected and stderr not preferred
    if (stream == stdout && !QtPrivate::shouldLogToStderr()) {
        OutputDebugStringA(str);
        return;
    }
#elif defined(Q_OS_ANDROID)
    __android_log_write(ANDROID_LOG_INFO, "QTestLib", str);
#endif
    outputString(str);
}

void QPlainTestLogger::printMessage(MessageSource source, const char *type, const char *msg,
                                    const char *file, int line)
{
    QTEST_ASSERT(type);
    QTEST_ASSERT(msg);

    QTestCharBuffer messagePrefix;

    QTestCharBuffer messageLocation;
#ifdef Q_OS_WIN
    constexpr const char *INCIDENT_LOCATION_STR = "\n%s(%d) : failure location";
    constexpr const char *OTHER_LOCATION_STR = "\n%s(%d) : message location";
#else
    constexpr const char *INCIDENT_LOCATION_STR = "\n   Loc: [%s(%d)]";
    constexpr const char *OTHER_LOCATION_STR = INCIDENT_LOCATION_STR;
#endif

    if (file) {
        switch (source) {
        case MessageSource::Incident:
            QTest::qt_asprintf(&messageLocation, INCIDENT_LOCATION_STR, file, line);
            break;
        case MessageSource::Other:
            QTest::qt_asprintf(&messageLocation, OTHER_LOCATION_STR, file, line);
            break;
        }
    }

    const char *msgFiller = msg[0] ? " " : "";
    QTestCharBuffer testIdentifier;
    QTestPrivate::generateTestIdentifier(&testIdentifier);
    QTest::qt_asprintf(&messagePrefix, "%s: %s%s%s%s\n",
                       type, testIdentifier.data(), msgFiller, msg, messageLocation.data());

    // In colored mode, printf above stripped our nonprintable control characters.
    // Put them back.
    memcpy(messagePrefix.data(), type, strlen(type));

    outputMessage(messagePrefix.data());
}

void QPlainTestLogger::printBenchmarkResult(const QBenchmarkResult &result)
{
    const char *bmtag = QTest::benchmarkResult2String();

    char buf1[1024];
    qsnprintf(
        buf1, sizeof(buf1), "%s: %s::%s",
        bmtag,
        QTestResult::currentTestObjectName(),
        result.context.slotName.toLatin1().data());

    char bufTag[1024];
    bufTag[0] = 0;
    QByteArray tag = result.context.tag.toLocal8Bit();
    if (tag.isEmpty() == false) {
        qsnprintf(bufTag, sizeof(bufTag), ":\"%s\"", tag.data());
    }


    char fillFormat[8];
    int fillLength = 5;
    qsnprintf(fillFormat, sizeof(fillFormat), ":\n%%%ds", fillLength);
    char fill[1024];
    qsnprintf(fill, sizeof(fill), fillFormat, "");

    const char * unitText = QTest::benchmarkMetricUnit(result.metric);

    qreal valuePerIteration = qreal(result.value) / qreal(result.iterations);
    char resultBuffer[100] = "";
    QTest::formatResult(resultBuffer, 100, valuePerIteration, QTest::countSignificantDigits(result.value));

    char buf2[1024];
    qsnprintf(buf2, sizeof(buf2), "%s %s", resultBuffer, unitText);

    char buf2_[1024];
    QByteArray iterationText = " per iteration";
    Q_ASSERT(result.iterations > 0);
    qsnprintf(buf2_, sizeof(buf2_), "%s", iterationText.data());

    char buf3[1024];
    Q_ASSERT(result.iterations > 0);
    QTest::formatResult(resultBuffer, 100, result.value, QTest::countSignificantDigits(result.value));
    qsnprintf(buf3, sizeof(buf3), " (total: %s, iterations: %d)", resultBuffer, result.iterations);

    char buf[1024];

    if (result.setByMacro) {
        qsnprintf(buf, sizeof(buf), "%s%s%s%s%s%s\n", buf1, bufTag, fill, buf2, buf2_, buf3);
    } else {
        qsnprintf(buf, sizeof(buf), "%s%s%s%s\n", buf1, bufTag, fill, buf2);
    }

    memcpy(buf, bmtag, strlen(bmtag));
    outputMessage(buf);
}

QPlainTestLogger::QPlainTestLogger(const char *filename)
    : QAbstractTestLogger(filename)
{
}

QPlainTestLogger::~QPlainTestLogger() = default;

void QPlainTestLogger::startLogging()
{
    QAbstractTestLogger::startLogging();

    char buf[1024];
    if (QTestLog::verboseLevel() < 0) {
        qsnprintf(buf, sizeof(buf), "Testing %s\n", QTestResult::currentTestObjectName());
    } else {
        qsnprintf(buf, sizeof(buf),
                  "********* Start testing of %s *********\n"
                  "Config: Using QtTest library " QTEST_VERSION_STR
                  ", %s, %s %s\n", QTestResult::currentTestObjectName(), QLibraryInfo::build(),
                  qPrintable(QSysInfo::productType()), qPrintable(QSysInfo::productVersion()));
    }
    outputMessage(buf);
}

void QPlainTestLogger::stopLogging()
{
    char buf[1024];
    const int timeMs = qRound(QTestLog::msecsTotalTime());
    if (QTestLog::verboseLevel() < 0) {
        qsnprintf(buf, sizeof(buf), "Totals: %d passed, %d failed, %d skipped, %d blacklisted, %dms\n",
                  QTestLog::passCount(), QTestLog::failCount(),
                  QTestLog::skipCount(), QTestLog::blacklistCount(), timeMs);
    } else {
        qsnprintf(buf, sizeof(buf),
                  "Totals: %d passed, %d failed, %d skipped, %d blacklisted, %dms\n"
                  "********* Finished testing of %s *********\n",
                  QTestLog::passCount(), QTestLog::failCount(),
                  QTestLog::skipCount(), QTestLog::blacklistCount(), timeMs,
                  QTestResult::currentTestObjectName());
    }
    outputMessage(buf);

    QAbstractTestLogger::stopLogging();
}


void QPlainTestLogger::enterTestFunction(const char * /*function*/)
{
    if (QTestLog::verboseLevel() >= 1)
        printMessage(MessageSource::Other, QTest::messageType2String(Info), "entering");
}

void QPlainTestLogger::leaveTestFunction()
{
}

void QPlainTestLogger::addIncident(IncidentTypes type, const char *description,
                                   const char *file, int line)
{
    // suppress B?PASS and B?XFAIL in silent mode
    if ((type == Pass || type == BlacklistedPass || type == XFail || type == BlacklistedXFail)
        && QTestLog::verboseLevel() < 0)
        return;

    printMessage(MessageSource::Incident, QTest::incidentType2String(type), description, file, line);
}

void QPlainTestLogger::addBenchmarkResult(const QBenchmarkResult &result)
{
    // suppress benchmark results in silent mode
    if (QTestLog::verboseLevel() < 0)
        return;

    printBenchmarkResult(result);
}

void QPlainTestLogger::addMessage(QtMsgType type, const QMessageLogContext &context, const QString &message)
{
    QAbstractTestLogger::addMessage(type, context, message);
}

void QPlainTestLogger::addMessage(MessageTypes type, const QString &message,
                                  const char *file, int line)
{
    // suppress non-fatal messages in silent mode
    if (type != QFatal && QTestLog::verboseLevel() < 0)
        return;

    printMessage(MessageSource::Other, QTest::messageType2String(type), qPrintable(message), file, line);
}

QT_END_NAMESPACE
