/*
 *  Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
 *  Copyright (C) 2013 Felix Geyer <debfx@fobos.de>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 or (at your option)
 *  version 3 of the License.
 *
 *  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, see <http://www.gnu.org/licenses/>.
 */

#include <QTest>

#include "TestEntry.h"
#include "core/Clock.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/TimeInfo.h"
#include "crypto/Crypto.h"

QTEST_GUILESS_MAIN(TestEntry)

void TestEntry::initTestCase()
{
    QVERIFY(Crypto::init());
    QLocale::setDefault(QLocale::c());
}

void TestEntry::testHistoryItemDeletion()
{
    QScopedPointer<Entry> entry(new Entry());
    QPointer<Entry> historyEntry = new Entry();

    entry->addHistoryItem(historyEntry);
    QCOMPARE(entry->historyItems().size(), 1);

    QList<Entry*> historyEntriesToRemove;
    historyEntriesToRemove.append(historyEntry);
    entry->removeHistoryItems(historyEntriesToRemove);
    QCOMPARE(entry->historyItems().size(), 0);
    QVERIFY(historyEntry.isNull());
}

void TestEntry::testCopyDataFrom()
{
    QScopedPointer<Entry> entry(new Entry());

    entry->setTitle("testtitle");
    entry->attributes()->set("attr1", "abc");
    entry->attributes()->set("attr2", "def");

    entry->attachments()->set("test", "123");
    entry->attachments()->set("test2", "456");

    AutoTypeAssociations::Association assoc;
    assoc.window = "1";
    assoc.sequence = "2";
    entry->autoTypeAssociations()->add(assoc);
    assoc.window = "3";
    assoc.sequence = "4";
    entry->autoTypeAssociations()->add(assoc);

    QScopedPointer<Entry> entry2(new Entry());
    entry2->copyDataFrom(entry.data());

    QCOMPARE(entry2->title(), QString("testtitle"));
    QCOMPARE(entry2->attributes()->value("attr1"), QString("abc"));
    QCOMPARE(entry2->attributes()->value("attr2"), QString("def"));

    QCOMPARE(entry2->attachments()->keys().size(), 2);
    QCOMPARE(entry2->attachments()->value("test"), QByteArray("123"));
    QCOMPARE(entry2->attachments()->value("test2"), QByteArray("456"));

    QCOMPARE(entry2->autoTypeAssociations()->size(), 2);
    QCOMPARE(entry2->autoTypeAssociations()->get(0).window, QString("1"));
    QCOMPARE(entry2->autoTypeAssociations()->get(1).window, QString("3"));
}

void TestEntry::testClone()
{
    QScopedPointer<Entry> entryOrg(new Entry());
    entryOrg->setUuid(QUuid::createUuid());
    entryOrg->setPassword("pass");
    entryOrg->setTitle("Original Title");
    entryOrg->beginUpdate();
    entryOrg->setTitle("New Title");
    entryOrg->endUpdate();
    TimeInfo entryOrgTime = entryOrg->timeInfo();
    QDateTime dateTime = Clock::datetimeUtc(60);
    entryOrgTime.setCreationTime(dateTime);
    entryOrg->setTimeInfo(entryOrgTime);

    QScopedPointer<Entry> entryCloneNone(entryOrg->clone(Entry::CloneNoFlags));
    QCOMPARE(entryCloneNone->uuid(), entryOrg->uuid());
    QCOMPARE(entryCloneNone->title(), QString("New Title"));
    QCOMPARE(entryCloneNone->historyItems().size(), 0);
    QCOMPARE(entryCloneNone->timeInfo().creationTime(), entryOrg->timeInfo().creationTime());

    QScopedPointer<Entry> entryCloneNewUuid(entryOrg->clone(Entry::CloneNewUuid));
    QVERIFY(entryCloneNewUuid->uuid() != entryOrg->uuid());
    QVERIFY(!entryCloneNewUuid->uuid().isNull());
    QCOMPARE(entryCloneNewUuid->title(), QString("New Title"));
    QCOMPARE(entryCloneNewUuid->historyItems().size(), 0);
    QCOMPARE(entryCloneNewUuid->timeInfo().creationTime(), entryOrg->timeInfo().creationTime());

    // Reset modification time
    entryOrgTime.setLastModificationTime(Clock::datetimeUtc(60));
    entryOrg->setTimeInfo(entryOrgTime);

    QScopedPointer<Entry> entryCloneRename(entryOrg->clone(Entry::CloneRenameTitle));
    QCOMPARE(entryCloneRename->uuid(), entryOrg->uuid());
    QCOMPARE(entryCloneRename->title(), QString("New Title - Clone"));
    // Cloning should not modify time info unless explicitly requested
    QCOMPARE(entryCloneRename->timeInfo(), entryOrg->timeInfo());

    QScopedPointer<Entry> entryCloneResetTime(entryOrg->clone(Entry::CloneResetTimeInfo));
    QCOMPARE(entryCloneResetTime->uuid(), entryOrg->uuid());
    QCOMPARE(entryCloneResetTime->title(), QString("New Title"));
    QCOMPARE(entryCloneResetTime->historyItems().size(), 0);
    QVERIFY(entryCloneResetTime->timeInfo().creationTime() != entryOrg->timeInfo().creationTime());

    // Date back history of original entry
    Entry* firstHistoryItem = entryOrg->historyItems()[0];
    TimeInfo entryOrgHistoryTimeInfo = firstHistoryItem->timeInfo();
    QDateTime datedBackEntryOrgModificationTime = entryOrgHistoryTimeInfo.lastModificationTime().addMSecs(-10);
    entryOrgHistoryTimeInfo.setLastModificationTime(datedBackEntryOrgModificationTime);
    entryOrgHistoryTimeInfo.setCreationTime(datedBackEntryOrgModificationTime);
    firstHistoryItem->setTimeInfo(entryOrgHistoryTimeInfo);

    QScopedPointer<Entry> entryCloneHistory(entryOrg->clone(Entry::CloneIncludeHistory | Entry::CloneResetTimeInfo));
    QCOMPARE(entryCloneHistory->uuid(), entryOrg->uuid());
    QCOMPARE(entryCloneHistory->title(), QString("New Title"));
    QCOMPARE(entryCloneHistory->historyItems().size(), entryOrg->historyItems().size());
    QCOMPARE(entryCloneHistory->historyItems().at(0)->title(), QString("Original Title"));
    QVERIFY(entryCloneHistory->timeInfo().creationTime() != entryOrg->timeInfo().creationTime());
    // Timeinfo of history items should not be modified
    QList<Entry*> entryOrgHistory = entryOrg->historyItems(), clonedHistory = entryCloneHistory->historyItems();
    auto entryOrgHistoryItem = entryOrgHistory.constBegin();
    for (auto entryCloneHistoryItem = clonedHistory.constBegin(); entryCloneHistoryItem != clonedHistory.constEnd();
         ++entryCloneHistoryItem, ++entryOrgHistoryItem) {
        QCOMPARE((*entryOrgHistoryItem)->timeInfo(), (*entryCloneHistoryItem)->timeInfo());
    }

    Database db;
    auto* entryOrgClone = entryOrg->clone(Entry::CloneNoFlags);
    entryOrgClone->setGroup(db.rootGroup());

    Entry* entryCloneUserRef = entryOrgClone->clone(Entry::CloneUserAsRef);
    entryCloneUserRef->setGroup(db.rootGroup());
    QCOMPARE(entryCloneUserRef->uuid(), entryOrgClone->uuid());
    QCOMPARE(entryCloneUserRef->title(), QString("New Title"));
    QCOMPARE(entryCloneUserRef->historyItems().size(), 0);
    QCOMPARE(entryCloneUserRef->timeInfo().creationTime(), entryOrgClone->timeInfo().creationTime());
    QVERIFY(entryCloneUserRef->attributes()->isReference(EntryAttributes::UserNameKey));
    QCOMPARE(entryCloneUserRef->resolvePlaceholder(entryCloneUserRef->username()), entryOrgClone->username());

    Entry* entryClonePassRef = entryOrgClone->clone(Entry::ClonePassAsRef);
    entryClonePassRef->setGroup(db.rootGroup());
    QCOMPARE(entryClonePassRef->uuid(), entryOrgClone->uuid());
    QCOMPARE(entryClonePassRef->title(), QString("New Title"));
    QCOMPARE(entryClonePassRef->historyItems().size(), 0);
    QCOMPARE(entryClonePassRef->timeInfo().creationTime(), entryOrgClone->timeInfo().creationTime());
    QVERIFY(entryClonePassRef->attributes()->isReference(EntryAttributes::PasswordKey));
    QCOMPARE(entryClonePassRef->resolvePlaceholder(entryCloneUserRef->password()), entryOrg->password());
    QCOMPARE(entryClonePassRef->attributes()->referenceUuid(EntryAttributes::PasswordKey), entryOrgClone->uuid());
}

void TestEntry::testResolveUrl()
{
    QScopedPointer<Entry> entry(new Entry());
    QString testUrl("www.google.com");
    QString testCmd("cmd://firefox " + testUrl);
    QString testFileUnix("/home/example/test.txt");
    QString testFileWindows("c:/WINDOWS/test.txt");
    QString testComplexCmd("cmd://firefox --start-now --url 'http://" + testUrl + "' --quit");
    QString nonHttpUrl("ftp://google.com");
    QString noUrl("random text inserted here");

    // Test standard URL's
    QCOMPARE(entry->resolveUrl(""), QString(""));
    QCOMPARE(entry->resolveUrl(testUrl), "https://" + testUrl);
    QCOMPARE(entry->resolveUrl("http://" + testUrl), "http://" + testUrl);
    // Test file:// URL's
    QCOMPARE(entry->resolveUrl("file://" + testFileUnix), "file://" + testFileUnix);
    QCOMPARE(entry->resolveUrl(testFileUnix), "file://" + testFileUnix);
    QCOMPARE(entry->resolveUrl("file:///" + testFileWindows), "file:///" + testFileWindows);
    QCOMPARE(entry->resolveUrl(testFileWindows), "file:///" + testFileWindows);
    // Test cmd:// with no URL
    QCOMPARE(entry->resolveUrl("cmd://firefox"), QString(""));
    QCOMPARE(entry->resolveUrl("cmd://firefox --no-url"), QString(""));
    // Test cmd:// with URL's
    QCOMPARE(entry->resolveUrl(testCmd), "https://" + testUrl);
    QCOMPARE(entry->resolveUrl(testComplexCmd), "http://" + testUrl);
    // Test non-http URL
    QCOMPARE(entry->resolveUrl(nonHttpUrl), QString(""));
    // Test no URL
    QCOMPARE(entry->resolveUrl(noUrl), QString(""));
}

void TestEntry::testResolveUrlPlaceholders()
{
    Entry entry;
    entry.setUrl("https://user:pw@keepassxc.org:80/path/example.php?q=e&s=t+2#fragment");

    QString rmvscm("//user:pw@keepassxc.org:80/path/example.php?q=e&s=t+2#fragment"); // Entry URL without scheme name.
    QString scm("https"); // Scheme name of the entry URL.
    QString host("keepassxc.org"); // Host component of the entry URL.
    QString port("80"); // Port number of the entry URL.
    QString path("/path/example.php"); // Path component of the entry URL.
    QString query("q=e&s=t+2"); // Query information of the entry URL.
    QString userinfo("user:pw"); // User information of the entry URL.
    QString username("user"); // User name of the entry URL.
    QString password("pw"); // Password of the entry URL.
    QString fragment("fragment"); // Fragment of the entry URL.

    QCOMPARE(entry.resolvePlaceholder("{URL:RMVSCM}"), rmvscm);
    QCOMPARE(entry.resolvePlaceholder("{URL:WITHOUTSCHEME}"), rmvscm);
    QCOMPARE(entry.resolvePlaceholder("{URL:SCM}"), scm);
    QCOMPARE(entry.resolvePlaceholder("{URL:SCHEME}"), scm);
    QCOMPARE(entry.resolvePlaceholder("{URL:HOST}"), host);
    QCOMPARE(entry.resolvePlaceholder("{URL:PORT}"), port);
    QCOMPARE(entry.resolvePlaceholder("{URL:PATH}"), path);
    QCOMPARE(entry.resolvePlaceholder("{URL:QUERY}"), query);
    QCOMPARE(entry.resolvePlaceholder("{URL:USERINFO}"), userinfo);
    QCOMPARE(entry.resolvePlaceholder("{URL:USERNAME}"), username);
    QCOMPARE(entry.resolvePlaceholder("{URL:PASSWORD}"), password);
    QCOMPARE(entry.resolvePlaceholder("{URL:FRAGMENT}"), fragment);
}

void TestEntry::testResolveRecursivePlaceholders()
{
    Database db;
    auto* root = db.rootGroup();

    auto* entry1 = new Entry();
    entry1->setGroup(root);
    entry1->setUuid(QUuid::createUuid());
    entry1->setTitle("{USERNAME}");
    entry1->setUsername("{PASSWORD}");
    entry1->setPassword("{URL}");
    entry1->setUrl("{S:CustomTitle}");
    entry1->attributes()->set("CustomTitle", "RecursiveValue");
    QCOMPARE(entry1->resolveMultiplePlaceholders(entry1->title()), QString("RecursiveValue"));

    auto* entry2 = new Entry();
    entry2->setGroup(root);
    entry2->setUuid(QUuid::createUuid());
    entry2->setTitle("Entry2Title");
    entry2->setUsername("{S:CustomUserNameAttribute}");
    entry2->setPassword(QString("{REF:P@I:%1}").arg(entry1->uuidToHex()));
    entry2->setUrl("http://{S:IpAddress}:{S:Port}/{S:Uri}");
    entry2->attributes()->set("CustomUserNameAttribute", "CustomUserNameValue");
    entry2->attributes()->set("IpAddress", "127.0.0.1");
    entry2->attributes()->set("Port", "1234");
    entry2->attributes()->set("Uri", "uri/path");

    auto* entry3 = new Entry();
    entry3->setGroup(root);
    entry3->setUuid(QUuid::createUuid());
    entry3->setTitle(QString("{REF:T@I:%1}").arg(entry2->uuidToHex()));
    entry3->setUsername(QString("{REF:U@I:%1}").arg(entry2->uuidToHex()));
    entry3->setPassword(QString("{REF:P@I:%1}").arg(entry2->uuidToHex()));
    entry3->setUrl(QString("{REF:A@I:%1}").arg(entry2->uuidToHex()));

    QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->title()), QString("Entry2Title"));
    QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->username()), QString("CustomUserNameValue"));
    QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->password()), QString("RecursiveValue"));
    QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->url()), QString("http://127.0.0.1:1234/uri/path"));

    auto* entry4 = new Entry();
    entry4->setGroup(root);
    entry4->setUuid(QUuid::createUuid());
    entry4->setTitle(QString("{REF:T@I:%1}").arg(entry3->uuidToHex()));
    entry4->setUsername(QString("{REF:U@I:%1}").arg(entry3->uuidToHex()));
    entry4->setPassword(QString("{REF:P@I:%1}").arg(entry3->uuidToHex()));
    entry4->setUrl(QString("{REF:A@I:%1}").arg(entry3->uuidToHex()));

    QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->title()), QString("Entry2Title"));
    QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->username()), QString("CustomUserNameValue"));
    QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->password()), QString("RecursiveValue"));
    QCOMPARE(entry4->resolveMultiplePlaceholders(entry4->url()), QString("http://127.0.0.1:1234/uri/path"));

    auto* entry5 = new Entry();
    entry5->setGroup(root);
    entry5->setUuid(QUuid::createUuid());
    entry5->attributes()->set("Scheme", "http");
    entry5->attributes()->set("Host", "host.org");
    entry5->attributes()->set("Port", "2017");
    entry5->attributes()->set("Path", "/some/path");
    entry5->attributes()->set("UserName", "username");
    entry5->attributes()->set("Password", "password");
    entry5->attributes()->set("Query", "q=e&t=s");
    entry5->attributes()->set("Fragment", "fragment");
    entry5->setUrl("{S:Scheme}://{S:UserName}:{S:Password}@{S:Host}:{S:Port}{S:Path}?{S:Query}#{S:Fragment}");
    entry5->setTitle("title+{URL:Path}+{URL:Fragment}+title");

    const QString url("http://username:password@host.org:2017/some/path?q=e&t=s#fragment");
    QCOMPARE(entry5->resolveMultiplePlaceholders(entry5->url()), url);
    QCOMPARE(entry5->resolveMultiplePlaceholders(entry5->title()), QString("title+/some/path+fragment+title"));

    auto* entry6 = new Entry();
    entry6->setGroup(root);
    entry6->setUuid(QUuid::createUuid());
    entry6->setTitle(QString("{REF:T@I:%1}").arg(entry3->uuidToHex()));
    entry6->setUsername(QString("{TITLE}"));
    entry6->setPassword(QString("{PASSWORD}"));

    QCOMPARE(entry6->resolvePlaceholder(entry6->title()), QString("Entry2Title"));
    QCOMPARE(entry6->resolvePlaceholder(entry6->username()), QString("Entry2Title"));
    QCOMPARE(entry6->resolvePlaceholder(entry6->password()), QString("{PASSWORD}"));

    auto* entry7 = new Entry();
    entry7->setGroup(root);
    entry7->setUuid(QUuid::createUuid());
    entry7->setTitle(QString("{REF:T@I:%1} and something else").arg(entry3->uuidToHex()));
    entry7->setUsername(QString("{TITLE}"));
    entry7->setPassword(QString("PASSWORD"));
    entry7->setNotes(QString("{lots} {of} {braces}"));

    QCOMPARE(entry7->resolvePlaceholder(entry7->title()), QString("Entry2Title and something else"));
    QCOMPARE(entry7->resolvePlaceholder(entry7->username()), QString("Entry2Title and something else"));
    QCOMPARE(entry7->resolvePlaceholder(entry7->password()), QString("PASSWORD"));
    QCOMPARE(entry7->resolvePlaceholder(entry7->notes()), QString("{lots} {of} {braces}"));
}

void TestEntry::testResolveReferencePlaceholders()
{
    Database db;
    auto* root = db.rootGroup();

    auto* entry1 = new Entry();
    entry1->setGroup(root);
    entry1->setUuid(QUuid::createUuid());
    entry1->setTitle("Title1");
    entry1->setUsername("Username1");
    entry1->setPassword("Password1");
    entry1->setUrl("Url1");
    entry1->setNotes("Notes1");
    entry1->attributes()->set("CustomAttribute1", "CustomAttributeValue1");

    auto* group = new Group();
    group->setParent(root);
    auto* entry2 = new Entry();
    entry2->setGroup(group);
    entry2->setUuid(QUuid::createUuid());
    entry2->setTitle("Title2");
    entry2->setUsername("Username2");
    entry2->setPassword("Password2");
    entry2->setUrl("Url2");
    entry2->setNotes("Notes2");
    entry2->attributes()->set("CustomAttribute2", "CustomAttributeValue2");

    auto* entry3 = new Entry();
    entry3->setGroup(group);
    entry3->setUuid(QUuid::createUuid());
    entry3->setTitle("{S:AttributeTitle}");
    entry3->setUsername("{S:AttributeUsername}");
    entry3->setPassword("{S:AttributePassword}");
    entry3->setUrl("{S:AttributeUrl}");
    entry3->setNotes("{S:AttributeNotes}");
    entry3->attributes()->set("AttributeTitle", "TitleValue");
    entry3->attributes()->set("AttributeUsername", "UsernameValue");
    entry3->attributes()->set("AttributePassword", "PasswordValue");
    entry3->attributes()->set("AttributeUrl", "UrlValue");
    entry3->attributes()->set("AttributeNotes", "NotesValue");

    auto* tstEntry = new Entry();
    tstEntry->setGroup(root);
    tstEntry->setUuid(QUuid::createUuid());

    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuidToHex())), entry1->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), entry1->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry1->username())), entry1->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@P:%1}").arg(entry1->password())), entry1->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@A:%1}").arg(entry1->url())), entry1->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@N:%1}").arg(entry1->notes())), entry1->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(
                 QString("{REF:T@O:%1}").arg(entry1->attributes()->value("CustomAttribute1"))),
             entry1->title());

    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry1->uuidToHex())), entry1->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry1->title())), entry1->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@U:%1}").arg(entry1->username())),
             entry1->username());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@P:%1}").arg(entry1->password())),
             entry1->password());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry1->url())), entry1->url());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry1->notes())), entry1->notes());

    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry2->uuidToHex())), entry2->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry2->title())), entry2->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@U:%1}").arg(entry2->username())), entry2->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@P:%1}").arg(entry2->password())), entry2->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@A:%1}").arg(entry2->url())), entry2->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@N:%1}").arg(entry2->notes())), entry2->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(
                 QString("{REF:T@O:%1}").arg(entry2->attributes()->value("CustomAttribute2"))),
             entry2->title());

    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@T:%1}").arg(entry2->title())), entry2->title());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@U:%1}").arg(entry2->username())),
             entry2->username());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@P:%1}").arg(entry2->password())),
             entry2->password());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@A:%1}").arg(entry2->url())), entry2->url());
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@N:%1}").arg(entry2->notes())), entry2->notes());

    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry3->uuidToHex())),
             entry3->attributes()->value("AttributeTitle"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(entry3->uuidToHex())),
             entry3->attributes()->value("AttributeUsername"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(entry3->uuidToHex())),
             entry3->attributes()->value("AttributePassword"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(entry3->uuidToHex())),
             entry3->attributes()->value("AttributeUrl"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(entry3->uuidToHex())),
             entry3->attributes()->value("AttributeNotes"));

    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:T@I:%1}").arg(entry3->uuidToHex().toUpper())),
             entry3->attributes()->value("AttributeTitle"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:U@I:%1}").arg(entry3->uuidToHex().toUpper())),
             entry3->attributes()->value("AttributeUsername"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:P@I:%1}").arg(entry3->uuidToHex().toUpper())),
             entry3->attributes()->value("AttributePassword"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:A@I:%1}").arg(entry3->uuidToHex().toUpper())),
             entry3->attributes()->value("AttributeUrl"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:N@I:%1}").arg(entry3->uuidToHex().toUpper())),
             entry3->attributes()->value("AttributeNotes"));

    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:t@i:%1}").arg(entry3->uuidToHex().toLower())),
             entry3->attributes()->value("AttributeTitle"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:u@i:%1}").arg(entry3->uuidToHex().toLower())),
             entry3->attributes()->value("AttributeUsername"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:p@i:%1}").arg(entry3->uuidToHex().toLower())),
             entry3->attributes()->value("AttributePassword"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:a@i:%1}").arg(entry3->uuidToHex().toLower())),
             entry3->attributes()->value("AttributeUrl"));
    QCOMPARE(tstEntry->resolveMultiplePlaceholders(QString("{REF:n@i:%1}").arg(entry3->uuidToHex().toLower())),
             entry3->attributes()->value("AttributeNotes"));
}

void TestEntry::testResolveUuidPlaceholder()
{
    Database db;
    auto* root = db.rootGroup();

    auto* entry = new Entry();
    entry->setGroup(root);
    entry->setUuid(QUuid::createUuid());
    entry->setTitle("Test Entry");
    entry->setUsername("TestUser");
    entry->setPassword("TestPass");
    entry->setNotes("Test with UUID: {UUID}");

    // Test that {UUID} placeholder resolves to the entry's UUID
    QString expectedUuid = entry->uuidToHex();
    QString resolvedNotes = entry->resolveMultiplePlaceholders(entry->notes());
    QCOMPARE(resolvedNotes, QString("Test with UUID: %1").arg(expectedUuid));

    // Test {UUID} placeholder directly
    QCOMPARE(entry->resolveMultiplePlaceholders("{UUID}"), expectedUuid);

    // Test case insensitivity
    QCOMPARE(entry->resolveMultiplePlaceholders("{uuid}"), expectedUuid);
    QCOMPARE(entry->resolveMultiplePlaceholders("{Uuid}"), expectedUuid);

    // Test mixed case in text
    QCOMPARE(entry->resolveMultiplePlaceholders("UUID is {UUID} here"), QString("UUID is %1 here").arg(expectedUuid));

    // Test advanced attribute with {REF:U@I:{UUID}} - should resolve to the entry's own username
    entry->attributes()->set("SelfReference", "{REF:U@I:{UUID}}");
    QString attributeValue = entry->attributes()->value("SelfReference");
    QString resolvedSelfRef = entry->resolveMultiplePlaceholders(attributeValue);

    // Test the manual reference to confirm it works as before
    QString manualReference = QString("{REF:U@I:%1}").arg(entry->uuidToHex());
    entry->attributes()->set("ManualReference", manualReference);
    QString resolvedManualRef = entry->resolveMultiplePlaceholders(entry->attributes()->value("ManualReference"));

    // Test that both approaches work
    QCOMPARE(resolvedManualRef, entry->username());
    QCOMPARE(resolvedSelfRef, entry->username());
}

void TestEntry::testResolveNonIdPlaceholdersToUuid()
{
    Database db;
    auto* root = db.rootGroup();

    auto* referencedEntryTitle = new Entry();
    referencedEntryTitle->setGroup(root);
    referencedEntryTitle->setTitle("myTitle");
    referencedEntryTitle->setUuid(QUuid::createUuid());

    auto* referencedEntryUsername = new Entry();
    referencedEntryUsername->setGroup(root);
    referencedEntryUsername->setUsername("myUser");
    referencedEntryUsername->setUuid(QUuid::createUuid());

    auto* referencedEntryPassword = new Entry();
    referencedEntryPassword->setGroup(root);
    referencedEntryPassword->setPassword("myPassword");
    referencedEntryPassword->setUuid(QUuid::createUuid());

    auto* referencedEntryUrl = new Entry();
    referencedEntryUrl->setGroup(root);
    referencedEntryUrl->setUrl("myUrl");
    referencedEntryUrl->setUuid(QUuid::createUuid());

    auto* referencedEntryNotes = new Entry();
    referencedEntryNotes->setGroup(root);
    referencedEntryNotes->setNotes("myNotes");
    referencedEntryNotes->setUuid(QUuid::createUuid());

    const QList<QChar> placeholders{'T', 'U', 'P', 'A', 'N'};
    for (const QChar& searchIn : placeholders) {
        const Entry* referencedEntry = nullptr;
        QString newEntryNotesRaw("{REF:I@%1:%2}");

        switch (searchIn.toLatin1()) {
        case 'T':
            referencedEntry = referencedEntryTitle;
            newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->title());
            break;
        case 'U':
            referencedEntry = referencedEntryUsername;
            newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->username());
            break;
        case 'P':
            referencedEntry = referencedEntryPassword;
            newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->password());
            break;
        case 'A':
            referencedEntry = referencedEntryUrl;
            newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->url());
            break;
        case 'N':
            referencedEntry = referencedEntryNotes;
            newEntryNotesRaw = newEntryNotesRaw.arg(searchIn, referencedEntry->notes());
            break;
        default:
            break;
        }

        auto* newEntry = new Entry();
        newEntry->setGroup(root);
        newEntry->setNotes(newEntryNotesRaw);

        const QString newEntryNotesResolved = newEntry->resolveMultiplePlaceholders(newEntry->notes());
        QCOMPARE(newEntryNotesResolved, referencedEntry->uuidToHex());
    }
}

void TestEntry::testResolveConversionPlaceholders()
{
    Database db;
    auto* root = db.rootGroup();

    auto* entry1 = new Entry();
    entry1->setGroup(root);
    entry1->setUuid(QUuid::createUuid());
    entry1->setTitle("Title1 {T-CONV:/{USERNAME}/lower/} {T-CONV:/{PASSWORD}/upper/}");
    entry1->setUsername("Username1");
    entry1->setPassword("Password1");
    entry1->setUrl("https://example.com/?test=3423&h=sdsds");

    auto* entry2 = new Entry();
    entry2->setGroup(root);
    entry2->setUuid(QUuid::createUuid());
    entry2->setTitle("Title2");
    entry2->setUsername(QString("{T-CONV:/{REF:U@I:%1}/UPPER/}").arg(entry1->uuidToHex()));
    entry2->setPassword(QString("{REF:P@I:%1}").arg(entry1->uuidToHex()));
    entry2->setUrl("cmd://ssh {USERNAME}@server.com -p {PASSWORD}");

    // Test complicated and nested conversions
    QCOMPARE(entry1->resolveMultiplePlaceholders(entry1->title()), QString("Title1 username1 PASSWORD1"));
    QCOMPARE(entry2->resolveMultiplePlaceholders(entry2->url()),
             QString("cmd://ssh USERNAME1@server.com -p Password1"));
    // Test base64 and hex conversions
    QCOMPARE(entry1->resolveMultiplePlaceholders("{T-CONV:/{PASSWORD}/base64/}"), QString("UGFzc3dvcmQx"));
    QCOMPARE(entry1->resolveMultiplePlaceholders("{T-CONV:/{PASSWORD}/hex/}"), QString("50617373776f726431"));
    // Test URL encode and decode
    auto encodedURL = entry1->resolveMultiplePlaceholders("{T-CONV:/{URL}/uri/}");
    QCOMPARE(encodedURL, QString("https%3A%2F%2Fexample.com%2F%3Ftest%3D3423%26h%3Dsdsds"));
    QCOMPARE(entry1->resolveMultiplePlaceholders(
                 "{T-CONV:/https%3A%2F%2Fexample.com%2F%3Ftest%3D3423%26h%3Dsdsds/uri-dec/}"),
             entry1->url());
    // Test invalid syntax
    QString error;
    entry1->resolveConversionPlaceholder("{T-CONV:/{USERNAME}/junk/}", &error);
    QVERIFY(!error.isEmpty());
    entry1->resolveConversionPlaceholder("{T-CONV:}", &error);
    QVERIFY(!error.isEmpty());
    // Check that error gets cleared
    entry1->resolveConversionPlaceholder("{T-CONV:/a/upper/}", &error);
    QVERIFY(error.isEmpty());
}

void TestEntry::testResolveReplacePlaceholders()
{
    Database db;
    auto* root = db.rootGroup();

    auto* entry1 = new Entry();
    entry1->setGroup(root);
    entry1->setUuid(QUuid::createUuid());
    entry1->setTitle("Title1");
    entry1->setUsername("Username1");
    entry1->setPassword("Password1");

    auto* entry2 = new Entry();
    entry2->setGroup(root);
    entry2->setUuid(QUuid::createUuid());
    entry2->setTitle("SAP server1 12345");
    entry2->setUsername(QString("{T-REPLACE-RX:/{REF:U@I:%1}/\\d$/2/}").arg(entry1->uuidToHex()));
    entry2->setPassword(QString("{REF:P@I:%1}").arg(entry1->uuidToHex()));
    entry2->setUrl(
        R"(cmd://sap.exe -system={T-REPLACE-RX:/{Title}/(?i)^(.* )?(\w+(?=(\s* \d+$)))\3/$2/} -client={T-REPLACE-RX:/{Title}/(?i)^.* (?=\d+$)//} -user={USERNAME} -pw={PASSWORD})");

    // Test complicated and nested replacements
    QCOMPARE(entry2->resolveMultiplePlaceholders(entry2->url()),
             QString("cmd://sap.exe -system=server1 -client=12345 -user=Username2 -pw=Password1"));

    auto* entry3 = new Entry();
    entry3->setGroup(root);
    entry3->setUuid(QUuid::createUuid());
    entry3->setTitle("Entry 3");
    entry3->setUsername("HMAC-SHA-256");
    entry3->setUrl("{T-REPLACE-RX:!{USERNAME}!\\{USERNAME\\}!!}");

    // Test escaped enclosures
    QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->url()), entry3->username());

    // Test invalid syntax
    QString error;
    entry1->resolveRegexPlaceholder("{T-REPLACE-RX:/{USERNAME}/.*+?/test/}", &error); // invalid regex
    QVERIFY(!error.isEmpty());
    entry1->resolveRegexPlaceholder("{T-REPLACE-RX:/{USERNAME}/.*/}", &error); // no replacement
    QVERIFY(!error.isEmpty());
    // Check that error gets cleared
    entry1->resolveRegexPlaceholder("{T-REPLACE-RX:/{USERNAME}/\\d/2/}", &error);
    QVERIFY(error.isEmpty());
}

void TestEntry::testResolveClonedEntry()
{
    Database db;
    auto* root = db.rootGroup();

    auto* original = new Entry();
    original->setGroup(root);
    original->setUuid(QUuid::createUuid());
    original->setTitle("Title");
    original->setUsername("SomeUsername");
    original->setPassword("SomePassword");

    QCOMPARE(original->resolveMultiplePlaceholders(original->username()), original->username());
    QCOMPARE(original->resolveMultiplePlaceholders(original->password()), original->password());

    // Top-level clones.
    Entry* clone1 = original->clone(Entry::CloneNewUuid);
    clone1->setGroup(root);
    Entry* clone2 = original->clone(Entry::CloneUserAsRef | Entry::CloneNewUuid);
    clone2->setGroup(root);
    Entry* clone3 = original->clone(Entry::ClonePassAsRef | Entry::CloneNewUuid);
    clone3->setGroup(root);
    Entry* clone4 = original->clone(Entry::CloneUserAsRef | Entry::ClonePassAsRef | Entry::CloneNewUuid);
    clone4->setGroup(root);

    QCOMPARE(clone1->resolveMultiplePlaceholders(clone1->username()), original->username());
    QCOMPARE(clone1->resolveMultiplePlaceholders(clone1->password()), original->password());
    QCOMPARE(clone2->resolveMultiplePlaceholders(clone2->username()), original->username());
    QCOMPARE(clone2->resolveMultiplePlaceholders(clone2->password()), original->password());
    QCOMPARE(clone3->resolveMultiplePlaceholders(clone3->username()), original->username());
    QCOMPARE(clone3->resolveMultiplePlaceholders(clone3->password()), original->password());
    QCOMPARE(clone4->resolveMultiplePlaceholders(clone4->username()), original->username());
    QCOMPARE(clone4->resolveMultiplePlaceholders(clone4->password()), original->password());

    // Second-level clones.
    Entry* cclone1 = clone4->clone(Entry::CloneNewUuid);
    cclone1->setGroup(root);
    Entry* cclone2 = clone4->clone(Entry::CloneUserAsRef | Entry::CloneNewUuid);
    cclone2->setGroup(root);
    Entry* cclone3 = clone4->clone(Entry::ClonePassAsRef | Entry::CloneNewUuid);
    cclone3->setGroup(root);
    Entry* cclone4 = clone4->clone(Entry::CloneUserAsRef | Entry::ClonePassAsRef | Entry::CloneNewUuid);
    cclone4->setGroup(root);

    QCOMPARE(cclone1->resolveMultiplePlaceholders(cclone1->username()), original->username());
    QCOMPARE(cclone1->resolveMultiplePlaceholders(cclone1->password()), original->password());
    QCOMPARE(cclone2->resolveMultiplePlaceholders(cclone2->username()), original->username());
    QCOMPARE(cclone2->resolveMultiplePlaceholders(cclone2->password()), original->password());
    QCOMPARE(cclone3->resolveMultiplePlaceholders(cclone3->username()), original->username());
    QCOMPARE(cclone3->resolveMultiplePlaceholders(cclone3->password()), original->password());
    QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->username()), original->username());
    QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->password()), original->password());

    // Change the original's attributes and make sure that the changes are tracked.
    QString oldUsername = original->username();
    QString oldPassword = original->password();
    original->setUsername("DifferentUsername");
    original->setPassword("DifferentPassword");

    QCOMPARE(clone1->resolveMultiplePlaceholders(clone1->username()), oldUsername);
    QCOMPARE(clone1->resolveMultiplePlaceholders(clone1->password()), oldPassword);
    QCOMPARE(clone2->resolveMultiplePlaceholders(clone2->username()), original->username());
    QCOMPARE(clone2->resolveMultiplePlaceholders(clone2->password()), oldPassword);
    QCOMPARE(clone3->resolveMultiplePlaceholders(clone3->username()), oldUsername);
    QCOMPARE(clone3->resolveMultiplePlaceholders(clone3->password()), original->password());
    QCOMPARE(clone4->resolveMultiplePlaceholders(clone4->username()), original->username());
    QCOMPARE(clone4->resolveMultiplePlaceholders(clone4->password()), original->password());

    QCOMPARE(cclone1->resolveMultiplePlaceholders(cclone1->username()), original->username());
    QCOMPARE(cclone1->resolveMultiplePlaceholders(cclone1->password()), original->password());
    QCOMPARE(cclone2->resolveMultiplePlaceholders(cclone2->username()), original->username());
    QCOMPARE(cclone2->resolveMultiplePlaceholders(cclone2->password()), original->password());
    QCOMPARE(cclone3->resolveMultiplePlaceholders(cclone3->username()), original->username());
    QCOMPARE(cclone3->resolveMultiplePlaceholders(cclone3->password()), original->password());
    QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->username()), original->username());
    QCOMPARE(cclone4->resolveMultiplePlaceholders(cclone4->password()), original->password());
}

void TestEntry::testIsRecycled()
{
    auto entry = new Entry();
    QVERIFY(!entry->isRecycled());

    Database db;
    Group* root = db.rootGroup();
    QVERIFY(root);
    entry->setGroup(root);
    QVERIFY(!entry->isRecycled());

    QVERIFY(db.metadata()->recycleBinEnabled());
    db.recycleEntry(entry);
    QVERIFY(entry->isRecycled());

    auto group1 = new Group();
    group1->setParent(root);

    auto entry1 = new Entry();
    entry1->setGroup(group1);
    QVERIFY(!entry1->isRecycled());
    db.recycleGroup(group1);
    QVERIFY(entry1->isRecycled());
}

void TestEntry::testMoveUpDown()
{
    Database db;
    Group* root = db.rootGroup();
    QVERIFY(root);

    auto entry0 = new Entry();
    QVERIFY(entry0);
    entry0->setGroup(root);
    auto entry1 = new Entry();
    QVERIFY(entry1);
    entry1->setGroup(root);
    auto entry2 = new Entry();
    QVERIFY(entry2);
    entry2->setGroup(root);
    auto entry3 = new Entry();
    QVERIFY(entry3);
    entry3->setGroup(root);
    // default order, straight
    QCOMPARE(root->entries().at(0), entry0);
    QCOMPARE(root->entries().at(1), entry1);
    QCOMPARE(root->entries().at(2), entry2);
    QCOMPARE(root->entries().at(3), entry3);

    entry0->moveDown();
    QCOMPARE(root->entries().at(0), entry1);
    QCOMPARE(root->entries().at(1), entry0);
    QCOMPARE(root->entries().at(2), entry2);
    QCOMPARE(root->entries().at(3), entry3);

    entry0->moveDown();
    QCOMPARE(root->entries().at(0), entry1);
    QCOMPARE(root->entries().at(1), entry2);
    QCOMPARE(root->entries().at(2), entry0);
    QCOMPARE(root->entries().at(3), entry3);

    entry0->moveDown();
    QCOMPARE(root->entries().at(0), entry1);
    QCOMPARE(root->entries().at(1), entry2);
    QCOMPARE(root->entries().at(2), entry3);
    QCOMPARE(root->entries().at(3), entry0);

    // no effect
    entry0->moveDown();
    QCOMPARE(root->entries().at(0), entry1);
    QCOMPARE(root->entries().at(1), entry2);
    QCOMPARE(root->entries().at(2), entry3);
    QCOMPARE(root->entries().at(3), entry0);

    entry0->moveUp();
    QCOMPARE(root->entries().at(0), entry1);
    QCOMPARE(root->entries().at(1), entry2);
    QCOMPARE(root->entries().at(2), entry0);
    QCOMPARE(root->entries().at(3), entry3);

    entry0->moveUp();
    QCOMPARE(root->entries().at(0), entry1);
    QCOMPARE(root->entries().at(1), entry0);
    QCOMPARE(root->entries().at(2), entry2);
    QCOMPARE(root->entries().at(3), entry3);

    entry0->moveUp();
    QCOMPARE(root->entries().at(0), entry0);
    QCOMPARE(root->entries().at(1), entry1);
    QCOMPARE(root->entries().at(2), entry2);
    QCOMPARE(root->entries().at(3), entry3);

    // no effect
    entry0->moveUp();
    QCOMPARE(root->entries().at(0), entry0);
    QCOMPARE(root->entries().at(1), entry1);
    QCOMPARE(root->entries().at(2), entry2);
    QCOMPARE(root->entries().at(3), entry3);

    entry2->moveUp();
    QCOMPARE(root->entries().at(0), entry0);
    QCOMPARE(root->entries().at(1), entry2);
    QCOMPARE(root->entries().at(2), entry1);
    QCOMPARE(root->entries().at(3), entry3);

    entry0->moveDown();
    QCOMPARE(root->entries().at(0), entry2);
    QCOMPARE(root->entries().at(1), entry0);
    QCOMPARE(root->entries().at(2), entry1);
    QCOMPARE(root->entries().at(3), entry3);

    entry3->moveUp();
    QCOMPARE(root->entries().at(0), entry2);
    QCOMPARE(root->entries().at(1), entry0);
    QCOMPARE(root->entries().at(2), entry3);
    QCOMPARE(root->entries().at(3), entry1);

    entry3->moveUp();
    QCOMPARE(root->entries().at(0), entry2);
    QCOMPARE(root->entries().at(1), entry3);
    QCOMPARE(root->entries().at(2), entry0);
    QCOMPARE(root->entries().at(3), entry1);

    entry2->moveDown();
    QCOMPARE(root->entries().at(0), entry3);
    QCOMPARE(root->entries().at(1), entry2);
    QCOMPARE(root->entries().at(2), entry0);
    QCOMPARE(root->entries().at(3), entry1);

    entry1->moveUp();
    QCOMPARE(root->entries().at(0), entry3);
    QCOMPARE(root->entries().at(1), entry2);
    QCOMPARE(root->entries().at(2), entry1);
    QCOMPARE(root->entries().at(3), entry0);
}

void TestEntry::testPreviousParentGroup()
{
    Database db;
    auto* root = db.rootGroup();
    root->setUuid(QUuid::createUuid());
    QVERIFY(!root->uuid().isNull());

    auto* group1 = new Group();
    group1->setUuid(QUuid::createUuid());
    group1->setParent(root);
    QVERIFY(!group1->uuid().isNull());

    auto* group2 = new Group();
    group2->setParent(root);
    group2->setUuid(QUuid::createUuid());
    QVERIFY(!group2->uuid().isNull());

    auto* entry = new Entry();
    QVERIFY(entry);
    QVERIFY(entry->previousParentGroupUuid().isNull());
    QVERIFY(!entry->previousParentGroup());

    entry->setGroup(root);
    QVERIFY(entry->previousParentGroupUuid().isNull());
    QVERIFY(!entry->previousParentGroup());

    // Previous parent shouldn't be recorded if new and old parent are the same
    entry->setGroup(root);
    QVERIFY(entry->previousParentGroupUuid().isNull());
    QVERIFY(!entry->previousParentGroup());

    entry->setGroup(group1);
    QVERIFY(entry->previousParentGroupUuid() == root->uuid());
    QVERIFY(entry->previousParentGroup() == root);

    entry->setGroup(group2);
    QVERIFY(entry->previousParentGroupUuid() == group1->uuid());
    QVERIFY(entry->previousParentGroup() == group1);

    entry->setGroup(group2);
    QVERIFY(entry->previousParentGroupUuid() == group1->uuid());
    QVERIFY(entry->previousParentGroup() == group1);
}
