/*
    This file is part of KDE
    SPDX-FileCopyrightText: 2006, 2008 David Faure <faure@kde.org>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "fileundomanagertest.h"

#include "../src/utils_p.h"

#include "mockcoredelegateextensions.h"
#include <kio/batchrenamejob.h>
#include <kio/copyjob.h>
#include <kio/deletejob.h>
#include <kio/fileundomanager.h>
#include <kio/mkdirjob.h>
#include <kio/mkpathjob.h>
#include <kio/paste.h>
#include <kio/pastejob.h>
#include <kio/restorejob.h>
#include <kioglobal_p.h>
#include <kprotocolinfo.h>

#include <QApplication>
#include <QClipboard>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QMimeData>
#include <QSignalSpy>
#include <QTest>
#include <qplatformdefs.h>

#include <KConfig>
#include <KConfigGroup>
#include <KUrlMimeData>

#include <cerrno>
#include <time.h>
#ifdef Q_OS_WIN
#include <sys/utime.h>
#else
#include <sys/time.h>
#include <utime.h>
#endif

QTEST_MAIN(FileUndoManagerTest)

using namespace KIO;

static QString homeTmpDir()
{
    return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QDir::separator();
}
static QString destDir()
{
    return homeTmpDir() + "destdir/";
}

static QString srcFile()
{
    return homeTmpDir() + "testfile";
}
static QString destFile()
{
    return destDir() + "testfile";
}

#ifndef Q_OS_WIN
static QString srcLink()
{
    return homeTmpDir() + "symlink";
}
static QString destLink()
{
    return destDir() + "symlink";
}
#endif

static QString srcSubDir()
{
    return homeTmpDir() + "subdir";
}
static QString destSubDir()
{
    return destDir() + "subdir";
}

static QList<QUrl> sourceList()
{
    QList<QUrl> lst;
    lst << QUrl::fromLocalFile(srcFile());
#ifndef Q_OS_WIN
    lst << QUrl::fromLocalFile(srcLink());
#endif
    return lst;
}

static void createTestFile(const QString &path, const char *contents)
{
    QFile f(path);
    if (!f.open(QIODevice::WriteOnly)) {
        qFatal("Couldn't create %s", qPrintable(path));
    }
    f.write(QByteArray(contents));
    f.close();
}

static void createTestSymlink(const QString &path)
{
    // Create symlink if it doesn't exist yet
    QT_STATBUF buf;
    if (QT_LSTAT(QFile::encodeName(path).constData(), &buf) != 0) {
        bool ok = KIOPrivate::createSymlink(QStringLiteral("/IDontExist"), path); // broken symlink
        if (!ok) {
            qFatal("couldn't create symlink: %s", strerror(errno));
        }
        QVERIFY(QT_LSTAT(QFile::encodeName(path).constData(), &buf) == 0);
        QVERIFY(Utils::isLinkMask(buf.st_mode));
    } else {
        QVERIFY(Utils::isLinkMask(buf.st_mode));
    }
    qDebug("symlink %s created", qPrintable(path));
    QVERIFY(QFileInfo(path).isSymLink());
}

static void checkTestDirectory(const QString &path)
{
    QVERIFY(QFileInfo(path).isDir());
    QVERIFY(QFileInfo(path + "/fileindir").isFile());
#ifndef Q_OS_WIN
    QVERIFY(QFileInfo(path + "/testlink").isSymLink());
#endif
    QVERIFY(QFileInfo(path + "/dirindir").isDir());
    QVERIFY(QFileInfo(path + "/dirindir/nested").isFile());
}

static void createTestDirectory(const QString &path)
{
    QDir dir;
    bool ok = dir.mkpath(path);
    if (!ok) {
        qFatal("couldn't create %s", qPrintable(path));
    }
    createTestFile(path + "/fileindir", "File in dir");
#ifndef Q_OS_WIN
    createTestSymlink(path + "/testlink");
#endif
    ok = dir.mkdir(path + "/dirindir");
    if (!ok) {
        qFatal("couldn't create %s", qPrintable(path));
    }
    createTestFile(path + "/dirindir/nested", "Nested");
    checkTestDirectory(path);
}

class TestUiInterface : public FileUndoManager::UiInterface
{
public:
    TestUiInterface()
        : FileUndoManager::UiInterface()
        , m_mockAskUserInterface(new MockAskUserInterface)
    {
        setShowProgressInfo(false);
    }
    void jobError(KIO::Job *job) override
    {
        m_errorCode = job->error();
        qWarning() << job->errorString();
    }
    bool copiedFileWasModified(const QUrl &src, const QUrl &dest, const QDateTime &srcTime, const QDateTime &destTime) override
    {
        Q_UNUSED(src);
        m_dest = dest;
        Q_UNUSED(srcTime);
        Q_UNUSED(destTime);
        return true;
    }
    void setNextReplyToConfirmDeletion(bool b)
    {
        m_nextReplyToConfirmDeletion = b;
    }
    QUrl dest() const
    {
        return m_dest;
    }
    int errorCode() const
    {
        return m_errorCode;
    }
    void clear()
    {
        m_dest = QUrl();
        m_errorCode = 0;
        m_mockAskUserInterface->clear();
    }
    MockAskUserInterface *askUserMockInterface() const
    {
        return m_mockAskUserInterface.get();
    }
    void virtual_hook(int id, void *data) override
    {
        if (id == HookGetAskUserActionInterface) {
            AskUserActionInterface **p = static_cast<AskUserActionInterface **>(data);
            *p = m_mockAskUserInterface.get();
            m_mockAskUserInterface->m_deleteResult = m_nextReplyToConfirmDeletion;
        }
    }

private:
    QUrl m_dest;
    std::unique_ptr<MockAskUserInterface> m_mockAskUserInterface;
    int m_errorCode = 0;
    bool m_nextReplyToConfirmDeletion = true;
};

void FileUndoManagerTest::initTestCase()
{
    qDebug("initTestCase");

    QStandardPaths::setTestModeEnabled(true);

    // Get kio_trash to share our environment so that it writes trashrc to the right kdehome
    qputenv("KIOWORKER_ENABLE_TESTMODE", "1");

    // Start with a clean base dir
    cleanupTestCase();

    if (!QFile::exists(homeTmpDir())) {
        bool ok = QDir().mkpath(homeTmpDir());
        if (!ok) {
            qFatal("Couldn't create %s", qPrintable(homeTmpDir()));
        }
    }

    createTestFile(srcFile(), "Hello world");
#ifndef Q_OS_WIN
    createTestSymlink(srcLink());
#endif
    createTestDirectory(srcSubDir());

    QDir().mkpath(destDir());
    QVERIFY(QFileInfo(destDir()).isDir());

    QVERIFY(!FileUndoManager::self()->isUndoAvailable());
    m_uiInterface = new TestUiInterface; // owned by FileUndoManager
    FileUndoManager::self()->setUiInterface(m_uiInterface);
}

void FileUndoManagerTest::cleanupTestCase()
{
    KIO::Job *job = KIO::del(QUrl::fromLocalFile(homeTmpDir()), KIO::HideProgressInfo);
    job->exec();
}

void FileUndoManagerTest::doUndo()
{
    QEventLoop eventLoop;
    connect(FileUndoManager::self(), &FileUndoManager::undoJobFinished, &eventLoop, &QEventLoop::quit);
    FileUndoManager::self()->undo();
    eventLoop.exec(QEventLoop::ExcludeUserInputEvents); // wait for undo job to finish
}

void FileUndoManagerTest::testCopyFiles()
{
    // Initially inspired from JobTest::copyFileToSamePartition()
    const QString destdir = destDir();
    QList<QUrl> lst = sourceList();
    const QUrl d = QUrl::fromLocalFile(destdir);
    KIO::CopyJob *job = KIO::copy(lst, d, KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordCopyJob(job);

    QSignalSpy spyUndoAvailable(FileUndoManager::self(), &FileUndoManager::undoAvailable);
    QVERIFY(spyUndoAvailable.isValid());
    QSignalSpy spyTextChanged(FileUndoManager::self(), &FileUndoManager::undoTextChanged);
    QVERIFY(spyTextChanged.isValid());

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    QVERIFY(QFile::exists(destFile()));
#ifndef Q_OS_WIN
    // Don't use QFile::exists, it's a broken symlink...
    QVERIFY(QFileInfo(destLink()).isSymLink());
#endif

    // might have to wait for dbus signal here... but this is currently disabled.
    // QTest::qWait( 20 );
    QVERIFY(FileUndoManager::self()->isUndoAvailable());
    QCOMPARE(spyUndoAvailable.count(), 1);
    QCOMPARE(spyTextChanged.count(), 1);
    m_uiInterface->clear();

    m_uiInterface->setNextReplyToConfirmDeletion(false); // act like the user didn't confirm
    FileUndoManager::self()->undo();
    auto *lastMock = m_uiInterface->askUserMockInterface();
    QCOMPARE(lastMock->m_askUserDeleteCalled, 1);
    QVERIFY(QFile::exists(destFile())); // nothing happened yet

    // OK, now do it
    m_uiInterface->clear();
    m_uiInterface->setNextReplyToConfirmDeletion(true);
    doUndo();

    QVERIFY(!FileUndoManager::self()->isUndoAvailable());
    QVERIFY(spyUndoAvailable.count() >= 2); // it's in fact 3, due to lock/unlock emitting it as well
    QCOMPARE(spyTextChanged.count(), 2);
    QCOMPARE(m_uiInterface->askUserMockInterface()->m_askUserDeleteCalled, 1);

    // Check that undo worked
    QVERIFY(!QFile::exists(destFile()));
#ifndef Q_OS_WIN
    QVERIFY(!QFile::exists(destLink()));
    QVERIFY(!QFileInfo(destLink()).isSymLink());
#endif
}

void FileUndoManagerTest::testMoveFiles()
{
    const QString destdir = destDir();
    QList<QUrl> lst = sourceList();
    const QUrl d = QUrl::fromLocalFile(destdir);
    KIO::CopyJob *job = KIO::move(lst, d, KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordCopyJob(job);

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    QVERIFY(!QFile::exists(srcFile())); // the source moved
    QVERIFY(QFile::exists(destFile()));
#ifndef Q_OS_WIN
    QVERIFY(!QFileInfo(srcLink()).isSymLink());
    // Don't use QFile::exists, it's a broken symlink...
    QVERIFY(QFileInfo(destLink()).isSymLink());
#endif

    doUndo();

    QVERIFY(QFile::exists(srcFile())); // the source is back
    QVERIFY(!QFile::exists(destFile()));
#ifndef Q_OS_WIN
    QVERIFY(QFileInfo(srcLink()).isSymLink());
    QVERIFY(!QFileInfo(destLink()).isSymLink());
#endif
}

void FileUndoManagerTest::testCopyDirectory()
{
    const QString destdir = destDir();
    QList<QUrl> lst;
    lst << QUrl::fromLocalFile(srcSubDir());
    const QUrl d = QUrl::fromLocalFile(destdir);
    KIO::CopyJob *job = KIO::copy(lst, d, KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordCopyJob(job);

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    checkTestDirectory(srcSubDir()); // src untouched
    checkTestDirectory(destSubDir());

    doUndo();

    checkTestDirectory(srcSubDir());
    QVERIFY(!QFile::exists(destSubDir()));
}

void FileUndoManagerTest::testCopyEmptyDirectory()
{
    const QString src = srcSubDir() + "/.emptydir";
    const QString destEmptyDir = destDir() + "/.emptydir";
    QDir().mkpath(src);
    KIO::CopyJob *job = KIO::copy({QUrl::fromLocalFile(src)}, QUrl::fromLocalFile(destEmptyDir), KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordCopyJob(job);

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    QVERIFY(QFileInfo(src).isDir()); // untouched
    QVERIFY(QFileInfo(destEmptyDir).isDir());

    doUndo();

    QVERIFY(QFileInfo(src).isDir()); // untouched
    QVERIFY(!QFile::exists(destEmptyDir));
}

void FileUndoManagerTest::testMoveDirectory()
{
    const QString destdir = destDir();
    QList<QUrl> lst;
    lst << QUrl::fromLocalFile(srcSubDir());
    const QUrl d = QUrl::fromLocalFile(destdir);
    KIO::CopyJob *job = KIO::move(lst, d, KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordCopyJob(job);

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    QVERIFY(!QFile::exists(srcSubDir()));
    checkTestDirectory(destSubDir());

    doUndo();

    checkTestDirectory(srcSubDir());
    QVERIFY(!QFile::exists(destSubDir()));
}

void FileUndoManagerTest::testRenameFile()
{
    const QUrl oldUrl = QUrl::fromLocalFile(srcFile());
    const QUrl newUrl = QUrl::fromLocalFile(srcFile() + ".new");
    QList<QUrl> lst;
    lst.append(oldUrl);
    QSignalSpy spyUndoAvailable(FileUndoManager::self(), &FileUndoManager::undoAvailable);
    QVERIFY(spyUndoAvailable.isValid());
    KIO::Job *job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordJob(FileUndoManager::Rename, lst, newUrl, job);

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    QVERIFY(!QFile::exists(srcFile()));
    QVERIFY(QFileInfo(newUrl.toLocalFile()).isFile());
    QCOMPARE(spyUndoAvailable.count(), 1);

    doUndo();

    QVERIFY(QFile::exists(srcFile()));
    QVERIFY(!QFileInfo(newUrl.toLocalFile()).isFile());
}

void FileUndoManagerTest::testRenameDir()
{
    const QUrl oldUrl = QUrl::fromLocalFile(srcSubDir());
    const QUrl newUrl = QUrl::fromLocalFile(srcSubDir() + ".new");
    QList<QUrl> lst;
    lst.append(oldUrl);
    KIO::Job *job = KIO::moveAs(oldUrl, newUrl, KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordJob(FileUndoManager::Rename, lst, newUrl, job);

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    QVERIFY(!QFile::exists(srcSubDir()));
    QVERIFY(QFileInfo(newUrl.toLocalFile()).isDir());

    doUndo();

    QVERIFY(QFile::exists(srcSubDir()));
    QVERIFY(!QFileInfo(newUrl.toLocalFile()).isDir());
}

void FileUndoManagerTest::testCreateSymlink()
{
#ifdef Q_OS_WIN
    QSKIP("Test skipped on Windows for lack of proper symlink support");
#endif
    const QUrl link = QUrl::fromLocalFile(homeTmpDir() + "newlink");
    const QString path = link.toLocalFile();
    QVERIFY(!QFile::exists(path));

    const QUrl target = QUrl::fromLocalFile(homeTmpDir() + "linktarget");
    const QString targetPath = target.toLocalFile();
    createTestFile(targetPath, "Link's Target");
    QVERIFY(QFile::exists(targetPath));

    KIO::CopyJob *job = KIO::link(target, link, KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordCopyJob(job);
    QVERIFY2(job->exec(), qPrintable(job->errorString()));
    QVERIFY(QFile::exists(path));
    QVERIFY(QFileInfo(path).isSymLink());

    // For undoing symlinks no confirmation is required. We delete it straight away.
    doUndo();

    QVERIFY(!QFile::exists(path));
}

void FileUndoManagerTest::testCreateDir()
{
    const QUrl url = QUrl::fromLocalFile(srcSubDir() + ".mkdir");
    const QString path = url.toLocalFile();
    QVERIFY(!QFile::exists(path));

    KIO::SimpleJob *job = KIO::mkdir(url);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordJob(FileUndoManager::Mkdir, QList<QUrl>(), url, job);
    QVERIFY2(job->exec(), qPrintable(job->errorString()));
    QVERIFY(QFile::exists(path));
    QVERIFY(QFileInfo(path).isDir());

    m_uiInterface->clear();
    m_uiInterface->setNextReplyToConfirmDeletion(false); // act like the user didn't confirm
    FileUndoManager::self()->undo();
    QCOMPARE(m_uiInterface->askUserMockInterface()->m_askUserDeleteCalled, 1);
    QVERIFY(QFile::exists(path)); // nothing happened yet

    // OK, now do it
    m_uiInterface->clear();
    m_uiInterface->setNextReplyToConfirmDeletion(true);
    doUndo();

    QVERIFY(!QFile::exists(path));
}

void FileUndoManagerTest::testMkpath()
{
    const QString parent = srcSubDir() + "mkpath";
    const QString path = parent + "/subdir";
    QVERIFY(!QFile::exists(path));
    const QUrl url = QUrl::fromLocalFile(path);

    KIO::Job *job = KIO::mkpath(url, QUrl(), KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordJob(FileUndoManager::Mkpath, QList<QUrl>(), url, job);
    QVERIFY(job->exec());
    QVERIFY(QFileInfo(path).isDir());

    m_uiInterface->clear();
    m_uiInterface->setNextReplyToConfirmDeletion(true);
    doUndo();

    QVERIFY(!FileUndoManager::self()->isUndoAvailable());
    QCOMPARE(m_uiInterface->askUserMockInterface()->m_askUserDeleteCalled, 1);

    QVERIFY(!QFile::exists(path));
}

void FileUndoManagerTest::testTrashFiles()
{
    if (!KProtocolInfo::isKnownProtocol(QStringLiteral("trash"))) {
        QSKIP("kio_trash not installed");
    }

    // Trash it all at once: the file, the symlink, the subdir.
    QList<QUrl> lst = sourceList();
    lst.append(QUrl::fromLocalFile(srcSubDir()));
    KIO::Job *job = KIO::trash(lst, KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordJob(FileUndoManager::Trash, lst, QUrl(QStringLiteral("trash:/")), job);

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    // Check that things got removed
    QVERIFY(!QFile::exists(srcFile()));
#ifndef Q_OS_WIN
    QVERIFY(!QFileInfo(srcLink()).isSymLink());
#endif
    QVERIFY(!QFile::exists(srcSubDir()));

    // check trash?
    // Let's just check that it's not empty. kio_trash has its own unit tests anyway.
    KConfig cfg(QStringLiteral("trashrc"), KConfig::SimpleConfig);
    QVERIFY(cfg.hasGroup(QStringLiteral("Status")));
    QCOMPARE(cfg.group(QStringLiteral("Status")).readEntry("Empty", true), false);

    doUndo();

    QVERIFY(QFile::exists(srcFile()));
#ifndef Q_OS_WIN
    QVERIFY(QFileInfo(srcLink()).isSymLink());
#endif
    QVERIFY(QFile::exists(srcSubDir()));

    // We can't check that the trash is empty; other partitions might have their own trash
}

void FileUndoManagerTest::testRestoreTrashedFiles()
{
    if (!KProtocolInfo::isKnownProtocol(QStringLiteral("trash"))) {
        QSKIP("kio_trash not installed");
    }

    // Trash it all at once: the file, the symlink, the subdir.
    const QFile::Permissions origPerms = QFileInfo(srcFile()).permissions();
    QList<QUrl> lst = sourceList();
    lst.append(QUrl::fromLocalFile(srcSubDir()));
    KIO::Job *job = KIO::trash(lst, KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    QVERIFY(job->exec());

    const QMap<QString, QString> metaData = job->metaData();
    QList<QUrl> trashUrls;
    for (const QUrl &src : std::as_const(lst)) {
        QMap<QString, QString>::ConstIterator it = metaData.find("trashURL-" + src.path());
        QVERIFY(it != metaData.constEnd());
        trashUrls.append(QUrl(it.value()));
    }

    // Restore from trash
    KIO::RestoreJob *restoreJob = KIO::restoreFromTrash(trashUrls, KIO::HideProgressInfo);
    restoreJob->setUiDelegate(nullptr);
    QVERIFY(restoreJob->exec());

    QVERIFY(QFile::exists(srcFile()));
    QCOMPARE(QFileInfo(srcFile()).permissions(), origPerms);
#ifndef Q_OS_WIN
    QVERIFY(QFileInfo(srcLink()).isSymLink());
#endif
    QVERIFY(QFile::exists(srcSubDir()));

    // TODO support for RestoreJob in FileUndoManager !!!
}

static void setTimeStamp(const QString &path)
{
#ifdef Q_OS_UNIX
    // Put timestamp in the past so that we can check that the
    // copy actually preserves it.
    struct timeval tp;
    gettimeofday(&tp, nullptr);
    struct utimbuf utbuf;
    utbuf.actime = tp.tv_sec + 30; // 30 seconds in the future
    utbuf.modtime = tp.tv_sec + 60; // 60 second in the future
    utime(QFile::encodeName(path).constData(), &utbuf);
    qDebug("Time changed for %s", qPrintable(path));
#endif
}

void FileUndoManagerTest::testModifyFileBeforeUndo()
{
    // based on testCopyDirectory (so that we check that it works for files in subdirs too)
    const QString destdir = destDir();
    const QList<QUrl> lst{QUrl::fromLocalFile(srcSubDir())};
    const QUrl dest = QUrl::fromLocalFile(destdir);
    KIO::CopyJob *job = KIO::copy(lst, dest, KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordCopyJob(job);

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    checkTestDirectory(srcSubDir()); // src untouched
    checkTestDirectory(destSubDir());
    const QString destFile = destSubDir() + "/fileindir";
    setTimeStamp(destFile); // simulate a modification of the file

    doUndo();

    // Check that TestUiInterface::copiedFileWasModified got called
    QCOMPARE(m_uiInterface->dest().toLocalFile(), destFile);

    checkTestDirectory(srcSubDir());
    QVERIFY(!QFile::exists(destSubDir()));
}

void FileUndoManagerTest::testPasteClipboardUndo()
{
    const QList<QUrl> urls(sourceList());
    QMimeData *mimeData = new QMimeData();
    mimeData->setUrls(urls);
    KIO::setClipboardDataCut(mimeData, true);
    QClipboard *clipboard = QApplication::clipboard();
    clipboard->setMimeData(mimeData);

    // Paste the contents of the clipboard and check its status
    QUrl destDirUrl = QUrl::fromLocalFile(destDir());
    KIO::Job *job = KIO::paste(mimeData, destDirUrl, KIO::HideProgressInfo);
    QVERIFY(job);
    QVERIFY(job->exec());

    // Check if the clipboard was updated after paste operation
    QList<QUrl> urls2;
    for (const QUrl &url : urls) {
        QUrl dUrl = destDirUrl.adjusted(QUrl::StripTrailingSlash);
        dUrl.setPath(dUrl.path() + '/' + url.fileName());
        urls2 << dUrl;
    }
    QList<QUrl> clipboardUrls = KUrlMimeData::urlsFromMimeData(clipboard->mimeData());
    QCOMPARE(clipboardUrls, urls2);

    // Check if the clipboard was updated after undo operation
    doUndo();
    clipboardUrls = KUrlMimeData::urlsFromMimeData(clipboard->mimeData());
    QCOMPARE(clipboardUrls, urls);
}

void FileUndoManagerTest::testBatchRename()
{
    auto createUrl = [](const QString &path) -> QUrl {
        return QUrl::fromLocalFile(homeTmpDir() + path);
    };

    QList<QUrl> srcList;
    srcList << createUrl("textfile.txt") << createUrl("mediafile.mkv") << createUrl("sourcefile.cpp");

    createTestFile(srcList.at(0).path(), "foo");
    createTestFile(srcList.at(1).path(), "foo");
    createTestFile(srcList.at(2).path(), "foo");

    KIO::Job *job = KIO::batchRename(srcList, QLatin1String("newfile###"), 1, QLatin1Char('#'), KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordJob(FileUndoManager::BatchRename, srcList, QUrl(), job);
    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    QVERIFY(QFile::exists(createUrl("newfile001.txt").path()));
    QVERIFY(QFile::exists(createUrl("newfile002.mkv").path()));
    QVERIFY(QFile::exists(createUrl("newfile003.cpp").path()));
    QVERIFY(!QFile::exists(srcList.at(0).path()));
    QVERIFY(!QFile::exists(srcList.at(1).path()));
    QVERIFY(!QFile::exists(srcList.at(2).path()));

    doUndo();

    QVERIFY(!QFile::exists(createUrl("newfile###.txt").path()));
    QVERIFY(!QFile::exists(createUrl("newfile###.mkv").path()));
    QVERIFY(!QFile::exists(createUrl("newfile###.cpp").path()));
    QVERIFY(QFile::exists(srcList.at(0).path()));
    QVERIFY(QFile::exists(srcList.at(1).path()));
    QVERIFY(QFile::exists(srcList.at(2).path()));
}

void FileUndoManagerTest::testUndoCopyOfDeletedFile()
{
    const QUrl source = QUrl::fromLocalFile(homeTmpDir() + QLatin1String("source.txt"));
    const QUrl dest = QUrl::fromLocalFile(homeTmpDir() + QLatin1String("copy.txt"));

    createTestFile(source.toLocalFile(), "foo");
    QVERIFY(QFileInfo::exists(source.toLocalFile()));

    {
        auto copyJob = KIO::copy(source, dest, KIO::HideProgressInfo);
        copyJob->setUiDelegate(nullptr);
        FileUndoManager::self()->recordCopyJob(copyJob);
        QVERIFY2(copyJob->exec(), qPrintable(copyJob->errorString()));
        QVERIFY(QFileInfo::exists(dest.toLocalFile()));
    }

    {
        auto deleteJob = KIO::del(dest, KIO::HideProgressInfo);
        deleteJob->setUiDelegate(nullptr);
        QVERIFY2(deleteJob->exec(), qPrintable(deleteJob->errorString()));
        QVERIFY(!QFileInfo::exists(dest.toLocalFile()));
    }

    QVERIFY(FileUndoManager::self()->isUndoAvailable());
    QSignalSpy spyUndoAvailable(FileUndoManager::self(), &FileUndoManager::undoAvailable);
    QVERIFY(spyUndoAvailable.isValid());
    doUndo();
    QVERIFY(spyUndoAvailable.count() >= 2); // it's in fact 3, due to lock/unlock emitting it as well
    QVERIFY(!spyUndoAvailable.at(0).at(0).toBool());
    QVERIFY(!FileUndoManager::self()->isUndoAvailable());
}

void FileUndoManagerTest::testErrorDuringMoveUndo()
{
    const QString destdir = destDir();
    QList<QUrl> lst{QUrl::fromLocalFile(srcFile())};
    KIO::CopyJob *job = KIO::move(lst, QUrl::fromLocalFile(destdir), KIO::HideProgressInfo);
    job->setUiDelegate(nullptr);
    FileUndoManager::self()->recordCopyJob(job);

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    QVERIFY(!QFile::exists(srcFile())); // the source moved
    QVERIFY(QFile::exists(destFile()));
    createTestFile(srcFile(), "I'm back");

    doUndo();

    QCOMPARE(m_uiInterface->errorCode(), KIO::ERR_FILE_ALREADY_EXIST);
    QVERIFY(QFile::exists(destFile())); // still there
}

void FileUndoManagerTest::testNoUndoForSkipAll()
{
    auto *undoManager = FileUndoManager::self();

    QTemporaryDir tempDir;
    const QString tempPath = tempDir.path();

    const QString destPath = tempPath + "/dest_dir";
    QVERIFY(QDir(tempPath).mkdir("dest_dir"));
    const QUrl destUrl = QUrl::fromLocalFile(destPath);

    const QList<QUrl> lst{QUrl::fromLocalFile(tempPath + "/file_a"), QUrl::fromLocalFile(tempPath + "/file_b")};
    for (const auto &url : lst) {
        createTestFile(url.toLocalFile(), "foo");
    }

    auto createJob = [&]() {
        return KIO::copy(lst, destUrl, KIO::HideProgressInfo);
    };

    KIO::CopyJob *job = createJob();
    job->setUiDelegate(nullptr);
    undoManager->recordCopyJob(job);

    QSignalSpy spyUndoAvailable(undoManager, &FileUndoManager::undoAvailable);
    QVERIFY(spyUndoAvailable.isValid());
    QSignalSpy spyTextChanged(undoManager, &FileUndoManager::undoTextChanged);
    QVERIFY(spyTextChanged.isValid());

    QVERIFY2(job->exec(), qPrintable(job->errorString()));

    // Src files still exist
    for (const auto &url : lst) {
        QVERIFY(QFile::exists(url.toLocalFile()));
    }

    // Files copied to dest
    for (const auto &url : lst) {
        QVERIFY(QFile::exists(destPath + '/' + url.fileName()));
    }

    // An undo command was recorded
    QCOMPARE(spyUndoAvailable.count(), 1);
    QCOMPARE(spyTextChanged.count(), 1);

    KIO::CopyJob *repeatCopy = createJob();
    // Copying the same files again to the same dest, and setting skip all
    repeatCopy->setAutoSkip(true);
    undoManager->recordCopyJob(repeatCopy);

    QVERIFY2(repeatCopy->exec(), qPrintable(repeatCopy->errorString()));

    // No new undo command was added since the job didn't actually copy anything
    QCOMPARE(spyUndoAvailable.count(), 1);
    QCOMPARE(spyTextChanged.count(), 1);
}

// TODO: add test (and fix bug) for  DND of remote urls / "Link here" (creates .desktop files) // Undo (doesn't do anything)
// TODO: add test for interrupting a moving operation and then using Undo - bug:91579

#include "moc_fileundomanagertest.cpp"
