/*
 * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
 * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <QtTest>

#include "searchterm.h"

/// The query grammar of the right-click search actions.
///
/// Asserted on the CONSTRUCTED STRING throughout, never on a query notmuch
/// refuses: notmuch's parser rejects almost nothing, so `from:((((` parses
/// cleanly and matches zero. A test expecting a failure would pass against
/// correct code and against broken code alike.
class TestSearchTerm : public QObject
{
    Q_OBJECT
private slots:
    void quotesAPlainValue();
    void escapesEmbeddedQuotes();
    void collapsesWhitespaceAndNewlines();
    void rejectsEmptyAndWhitespaceOnly();
    void capsAnOverlongSelection();
    void buildsAFieldTerm();
    void omitsAFieldWithNoValue();
    void buildsADateRangeForOneDay();
    void tagIsNotQuoted();
    void extendParenthesisesBothSides();
    void extendOntoAnEmptyQueryIsAReplace();
};

void TestSearchTerm::quotesAPlainValue()
{
    QCOMPARE(SearchTerm::quote(QStringLiteral("Quarterly report")),
             QStringLiteral("\"Quarterly report\""));
}

void TestSearchTerm::escapesEmbeddedQuotes()
{
    // A selection is arbitrary prose and can carry a quote. Unescaped, it ends
    // the quoted string early and the rest becomes stray query syntax, which
    // notmuch accepts and matches nothing on.
    QCOMPARE(SearchTerm::quote(QStringLiteral("say \"hello\" now")),
             QStringLiteral("\"say \\\"hello\\\" now\""));
}

void TestSearchTerm::collapsesWhitespaceAndNewlines()
{
    QCOMPARE(SearchTerm::quote(QStringLiteral("  two\n\nlines\there  ")),
             QStringLiteral("\"two lines here\""));
}

void TestSearchTerm::rejectsEmptyAndWhitespaceOnly()
{
    // An empty term must yield an empty string, which is what every caller
    // tests to decide whether to offer a menu entry at all.
    QVERIFY(SearchTerm::quote(QString()).isEmpty());
    QVERIFY(SearchTerm::quote(QStringLiteral("   \n\t ")).isEmpty());
}

void TestSearchTerm::capsAnOverlongSelection()
{
    // A multi-kilobyte selection is a mis-drag, not a query.
    const QString huge(5000, QLatin1Char('x'));
    const QString term = SearchTerm::quote(huge);
    QVERIFY2(term.size() < 300,
             qPrintable(QStringLiteral("term was %1 chars").arg(term.size())));
    QVERIFY(term.startsWith(QStringLiteral("\"xxx")));
    QVERIFY(term.endsWith(QLatin1Char('"')));
}

void TestSearchTerm::buildsAFieldTerm()
{
    QCOMPARE(SearchTerm::field(QStringLiteral("from"),
                               QStringLiteral("Foo <foo@example.org>")),
             QStringLiteral("from:\"Foo <foo@example.org>\""));
}

void TestSearchTerm::omitsAFieldWithNoValue()
{
    // A message with no Cc must not offer cc:"" , which parses cleanly and
    // matches nothing, so the entry would look enabled and do nothing.
    QVERIFY(SearchTerm::field(QStringLiteral("cc"), QString()).isEmpty());
}

void TestSearchTerm::buildsADateRangeForOneDay()
{
    // notmuch's date: range is inclusive at both ends, so one day is the day
    // named twice rather than the day and its successor.
    const QDate day(2026, 8, 14);
    QCOMPARE(SearchTerm::onDate(day),
             QStringLiteral("date:2026-08-14..2026-08-14"));
    QVERIFY(SearchTerm::onDate(QDate()).isEmpty());
}

void TestSearchTerm::tagIsNotQuoted()
{
    // A tag name is a token from a known vocabulary, not prose. Quoting one
    // is not wrong but reads badly in the bar, and the user edits that text.
    QCOMPARE(SearchTerm::tag(QStringLiteral("inbox")),
             QStringLiteral("tag:inbox"));
    // A tag containing a space is the exception and does need quoting.
    QCOMPARE(SearchTerm::tag(QStringLiteral("to do")),
             QStringLiteral("tag:\"to do\""));
    QVERIFY(SearchTerm::tag(QString()).isEmpty());
}

void TestSearchTerm::extendParenthesisesBothSides()
{
    // THE case this exists for. The bar may hold a hand-written disjunction,
    // and `a or b AND c` binds as `a or (b AND c)`: the result WIDENS a search
    // the user asked to narrow. Both sides are wrapped so neither can rebind.
    QCOMPARE(SearchTerm::extend(QStringLiteral("tag:inbox or tag:flagged"),
                                QStringLiteral("from:foo@example.org")),
             QStringLiteral("(tag:inbox or tag:flagged) AND (from:foo@example.org)"));
}

void TestSearchTerm::extendOntoAnEmptyQueryIsAReplace()
{
    // Rather than "() AND (x)", which matches nothing.
    QCOMPARE(SearchTerm::extend(QString(), QStringLiteral("tag:inbox")),
             QStringLiteral("tag:inbox"));
    QCOMPARE(SearchTerm::extend(QStringLiteral("   "),
                                QStringLiteral("tag:inbox")),
             QStringLiteral("tag:inbox"));
    // And an empty new term leaves the existing query alone.
    QCOMPARE(SearchTerm::extend(QStringLiteral("tag:inbox"), QString()),
             QStringLiteral("tag:inbox"));
}

QTEST_MAIN(TestSearchTerm)
#include "test_searchterm.moc"
