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

#include <QByteArray>
#include <QDir>
#include <QDebug>
#include <QFile>
#include <QLibraryInfo>
#include <QProcess>
#include <QTest>

using namespace Qt::Literals::StringLiterals;

class tst_lcheck : public QObject
{
    Q_OBJECT

public:
    tst_lcheck()
        : lcheck(QLibraryInfo::path(QLibraryInfo::BinariesPath) + "/lcheck"),
          dataDir(QFINDTESTDATA("testdata/"))
    {
    }

private:
    static QStringList extractValidationLines(const QByteArray &ba);

private slots:
    void invalidTranslations();
    void validTranslations();
    void allViolationsReported();
    void allPluralViolationsReported();
    void disabledPlaceMarkerWithPluralDoesNotCrash();
    void checkFinished_defaultDisabled();
    void checkFinished_enabled();
    void disableAcceleratorCheck();
    void disableWhitespaceCheck();
    void disablePunctuationCheck();
    void disablePlaceMarkerCheck();
    void outputToFile_optionWritesReport();

private:
    void doCompare(const QStringList &actual, const QString &expectedFn);

    QString lcheck;
    QString dataDir;
};

void tst_lcheck::doCompare(const QStringList &actual, const QString &expectedFn)
{
    QFile file(expectedFn);
    QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
    QStringList expected = QString(file.readAll()).trimmed().split('\n');

    int i = 0, ei = expected.size(), gi = actual.size();
    for (;; i++) {
        if (i == gi) {
            if (i == ei)
                return;
            gi = 0;
            break;
        } else if (i == ei) {
            ei = 0;
            break;
        } else if (!QRegularExpression(QRegularExpression::anchoredPattern(expected.at(i)))
                            .match(actual.at(i))
                            .hasMatch()) {
            while ((ei - 1) >= i && (gi - 1) >= i
                   && (QRegularExpression(QRegularExpression::anchoredPattern(expected.at(ei - 1)))
                               .match(actual.at(gi - 1)))
                              .hasMatch())
                ei--, gi--;
            break;
        }
    }
    QString diff;
    for (int j = qMax(0, i - 3); j < i; j++)
        diff += expected.at(j) + '\n';
    diff += "<<<<<<< got\n";
    for (int j = i; j < gi; j++) {
        diff += actual.at(j) + '\n';
        if (j >= i + 5) {
            diff += "...\n";
            break;
        }
    }
    diff += "=========\n";
    for (int j = i; j < ei; j++) {
        diff += expected.at(j) + '\n';
        if (j >= i + 5) {
            diff += "...\n";
            break;
        }
    }
    diff += ">>>>>>> expected\n";
    for (int j = ei; j < qMin(ei + 3, expected.size()); j++)
        diff += expected.at(j) + '\n';
    QFAIL(qPrintable("Output for " + expectedFn + " does not meet expectations:\n" + diff));
}

QStringList tst_lcheck::extractValidationLines(const QByteArray &ba)
{
    const QString out = QString::fromUtf8(ba);
    QStringList actual;
    for (const QString &line : out.split('\n')) {
        const QString trimmed = line.trimmed();
        if (!trimmed.isEmpty())
            actual << trimmed;
    }
    return actual;
}

void tst_lcheck::invalidTranslations()
{
    QProcess proc;
    proc.start(lcheck, { dataDir + "invalid_de.ts" });
    QVERIFY(proc.waitForFinished());
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 1);
    auto stderrOutput = proc.readAllStandardError();
    QVERIFY(stderrOutput.contains("Translation: Text nicht &gefunden"));
    QVERIFY(stderrOutput.contains("Translation: Text gefunden %1"));
}

void tst_lcheck::validTranslations()
{
    QProcess proc;
    proc.start(lcheck, { dataDir + "valid_de.ts" });
    QVERIFY(proc.waitForFinished());
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 0);
    const QByteArray err = proc.readAllStandardError();
    QVERIFY(!err.contains("Translation: "));
}

// Regression test for QTBUG-148562: all violations of the same check across
// multiple messages must be reported, not collapsed into one.
void tst_lcheck::allViolationsReported()
{
    QProcess proc;
    proc.start(lcheck, { dataDir + "multiple_same_check_de.ts" });
    QVERIFY(proc.waitForFinished());
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 1);

    const QByteArray err = proc.readAllStandardError();
    QCOMPARE(static_cast<int>(err.count("Translation does not end with the same punctuation")), 3);
    QVERIFY(err.contains("Translation: Hallo"));
    QVERIFY(err.contains("Translation: Tschuess"));
    QVERIFY(err.contains("Translation: Willkommen"));
}

// Regression test for QTBUG-148562: all numerus forms of a plural message that
// fail the same check must be reported.
void tst_lcheck::allPluralViolationsReported()
{
    QProcess proc;
    proc.start(lcheck, { dataDir + "plural_same_check_de.ts" });
    QVERIFY(proc.waitForFinished());
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 1);

    const QByteArray err = proc.readAllStandardError();
    QCOMPARE(static_cast<int>(err.count("Translation does not end with the same punctuation")), 2);
    QVERIFY(err.contains("Translation: %n Datei"));
    QVERIFY(err.contains("Translation: %n Dateien"));
}

// --no-place-marker skips gathering numerus info, leaving countRefNeeds empty.
// A plural message must not make the validator index that empty list.
void tst_lcheck::disabledPlaceMarkerWithPluralDoesNotCrash()
{
    QProcess proc;
    proc.start(lcheck, { "--no-place-marker", dataDir + "plural_same_check_de.ts" });
    QVERIFY(proc.waitForFinished());
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 1);
    QCOMPARE(static_cast<int>(proc.readAllStandardError().count(
                     "Translation does not end with the same punctuation")),
             2);
}

void tst_lcheck::checkFinished_defaultDisabled()
{
    QProcess proc;
    proc.start(lcheck, { dataDir + "finished_issues_de.ts" });
    QVERIFY(proc.waitForFinished());
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 0);

    const QByteArray err = proc.readAllStandardError();
    QVERIFY(!err.contains("Translation: Text nicht gefunden"));
    QVERIFY(!err.contains("Translation: Beenden"));
}

void tst_lcheck::checkFinished_enabled()
{
    QProcess proc;
    proc.start(lcheck, { "--check-finished", dataDir + "finished_issues_de.ts" });
    QVERIFY(proc.waitForFinished());
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 1);

    const QByteArray err = proc.readAllStandardError();
    QVERIFY(err.contains("Translation: Text nicht gefunden"));
    QVERIFY(err.contains("Translation: Beenden"));
}

void tst_lcheck::disableAcceleratorCheck()
{
    {
        QProcess proc;
        proc.start(lcheck, { dataDir + "accelerator_mismatch_de.ts" });
        QVERIFY(proc.waitForFinished());
        QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
        QCOMPARE(proc.exitCode(), 1);
        QVERIFY(proc.readAllStandardError().contains("Translation: Beenden"));
    }
    {
        QProcess proc;
        proc.start(lcheck, { "--no-accelerator", dataDir + "accelerator_mismatch_de.ts" });
        QVERIFY(proc.waitForFinished());
        QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
        QCOMPARE(proc.exitCode(), 0);
    }
}

void tst_lcheck::disableWhitespaceCheck()
{
    {
        QProcess proc;
        proc.start(lcheck, { dataDir + "whitespace_mismatch_de.ts" });
        QVERIFY(proc.waitForFinished());
        QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
        QCOMPARE(proc.exitCode(), 1);
        QVERIFY(proc.readAllStandardError().contains("Translation: Bereit "));
    }
    {
        QProcess proc;
        proc.start(lcheck, { "--no-whitespaces", dataDir + "whitespace_mismatch_de.ts" });
        QVERIFY(proc.waitForFinished());
        QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
        QCOMPARE(proc.exitCode(), 0);
    }
}

void tst_lcheck::disablePunctuationCheck()
{
    {
        QProcess proc;
        proc.start(lcheck, { dataDir + "punctuation_mismatch_de.ts" });
        QVERIFY(proc.waitForFinished());
        QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
        QCOMPARE(proc.exitCode(), 1);
        QVERIFY(proc.readAllStandardError().contains("Translation: Bereit"));
    }
    {
        QProcess proc;
        proc.start(lcheck, { "--no-punctuation", dataDir + "punctuation_mismatch_de.ts" });
        QVERIFY(proc.waitForFinished());
        QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
        QCOMPARE(proc.exitCode(), 0);
    }
}

void tst_lcheck::disablePlaceMarkerCheck()
{
    {
        QProcess proc;
        proc.start(lcheck, { dataDir + "placemarker_mismatch_de.ts" });
        QVERIFY(proc.waitForFinished());
        QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
        QCOMPARE(proc.exitCode(), 1);
        QVERIFY(proc.readAllStandardError().contains("Translation: Gefunden %1 Elemente"));
    }
    {
        QProcess proc;
        proc.start(lcheck, { "--no-place-marker", dataDir + "placemarker_mismatch_de.ts" });
        QVERIFY(proc.waitForFinished());
        QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
        QCOMPARE(proc.exitCode(), 0);
    }
}

void tst_lcheck::outputToFile_optionWritesReport()
{
    QTemporaryDir tmp;
    QVERIFY(tmp.isValid());
    const QString report = tmp.path() + "/report.txt";

    QProcess proc;
    proc.start(lcheck, { "-o", report, dataDir + "invalid_de.ts" });
    QVERIFY(proc.waitForFinished());
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 1);

    const QByteArray err = proc.readAllStandardError();
    QVERIFY(!err.contains("Translation: "));

    QFile f(report);
    QVERIFY(f.open(QIODevice::ReadOnly | QIODevice::Text));
    const QByteArray fileData = f.readAll();
    QVERIFY(!fileData.isEmpty());

    const QStringList actual = extractValidationLines(fileData);
    doCompare(actual, dataDir + "expected_invalid_report.txt");
}

QTEST_MAIN(tst_lcheck)
#include "tst_lcheck.moc"
