// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QTest>

#include <QCoreApplication>

#include <QTemporaryDir>
#include <QFileSystemWatcher>
#include <QElapsedTimer>
#include <QTextStream>
#include <QMap>
#include <QString>
#include <QDir>
#include <QSignalSpy>
#include <QTimer>
#include <QTemporaryFile>
#if defined(Q_OS_WIN)
#include <qt_windows.h>
#endif

#ifdef Q_OS_ANDROID
#include <QStandardPaths>
#endif

/* All tests need to run in temporary directories not used
 * by the application to avoid non-deterministic failures on Windows
 * due to locked directories and left-overs from previous tests. */

class tst_QFileSystemWatcher : public QObject
{
    Q_OBJECT
public:
    tst_QFileSystemWatcher();

private slots:
#ifdef QT_BUILD_INTERNAL
    void basicTest_data();
    void basicTest();

    void watchDirectory_data();
    void watchDirectory();
#endif

    void addPath();
    void removePath();
    void addPaths();
    void removePaths();
    void removePathsFilesInSameDirectory();

#ifdef QT_BUILD_INTERNAL
    void watchFileAndItsDirectory_data() { basicTest_data(); }
    void watchFileAndItsDirectory();
#endif

    void nonExistingFile();

    void removeFileAndUnWatch();

    void destroyAfterQCoreApplication();

#ifdef QT_BUILD_INTERNAL
    void QTBUG2331();
    void QTBUG2331_data() { basicTest_data(); }
#endif

    void signalsEmittedAfterFileMoved();

    void watchUnicodeCharacters();
#if defined(Q_OS_WIN)
    void watchDirectoryAttributeChanges();
#endif

private:
    QString m_tempDirPattern;
};

tst_QFileSystemWatcher::tst_QFileSystemWatcher()
{
    m_tempDirPattern = QDir::tempPath();
    if (!m_tempDirPattern.endsWith(QLatin1Char('/')))
        m_tempDirPattern += QLatin1Char('/');
    m_tempDirPattern += QStringLiteral("tst_qfilesystemwatcherXXXXXX");

#ifdef Q_OS_ANDROID
    QDir::setCurrent(QStandardPaths::writableLocation(QStandardPaths::CacheLocation));
#endif
}

#ifdef QT_BUILD_INTERNAL
void tst_QFileSystemWatcher::basicTest_data()
{
    QTest::addColumn<QString>("backend");
    QTest::addColumn<QString>("testFileName");
    const QString testFile = QStringLiteral("testfile.txt");
    // QTBUG-31341: Test the UNICODE capabilities; ensure no QString::toLower()
    // is in the code path since that will lower case for example
    // LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE with context, whereas the Windows file
    // system will not.
    const QString specialCharacterFile =
        QString(QChar(ushort(0x130))) // LATIN_CAPITAL_LETTER_I_WITH_DOT_ABOVE
        + QChar(ushort(0x00DC)) // LATIN_CAPITAL_LETTER_U_WITH_DIAERESIS
        + QStringLiteral(".txt");

#if !defined(QT_NO_INOTIFY)
    QTest::newRow("native backend-testfile") << "native" << testFile;
    QTest::newRow("native backend-specialchars") << "native" << specialCharacterFile;
#endif
    QTest::newRow("poller backend-testfile") << "poller" << testFile;
}

void tst_QFileSystemWatcher::basicTest()
{
    QFETCH(QString, backend);
    QFETCH(QString, testFileName);

    // create test file
    QTemporaryDir temporaryDirectory(m_tempDirPattern);
    QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));
    QFile testFile(temporaryDirectory.path() + QLatin1Char('/') + testFileName);
    QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate));
    testFile.write(QByteArray("hello"));
    testFile.close();

    // set some file permissions
    testFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner);

    // create watcher, forcing it to use a specific backend
    QFileSystemWatcher watcher;
    watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend);
    QVERIFY(watcher.addPath(testFile.fileName()));

    QSignalSpy changedSpy(&watcher, &QFileSystemWatcher::fileChanged);
    QVERIFY(changedSpy.isValid());
    QEventLoop eventLoop;
    QTimer timer;
    connect(&timer, SIGNAL(timeout()), &eventLoop, SLOT(quit()));

    // modify the file, should get a signal from the watcher

    // resolution of the modification time is system dependent, but it's at most 1 second when using
    // the polling engine. I've heard rumors that FAT32 has a 2 second resolution. So, we have to
    // wait a bit before we can modify the file (hrmph)...
    QTest::qWait(2000);

    testFile.open(QIODevice::WriteOnly | QIODevice::Append);
    testFile.write(QByteArray("world"));
    testFile.close();

    // waiting max 5 seconds for notification for file modification to trigger
    QTRY_COMPARE(changedSpy.count(), 1);
    QCOMPARE(changedSpy.at(0).count(), 1);

    QString fileName = changedSpy.at(0).at(0).toString();
    QCOMPARE(fileName, testFile.fileName());

    changedSpy.clear();

    // remove the watch and modify the file, should not get a signal from the watcher
    QVERIFY(watcher.removePath(testFile.fileName()));
    testFile.open(QIODevice::WriteOnly | QIODevice::Truncate);
    testFile.write(QByteArray("hello universe!"));
    testFile.close();

    // waiting max 5 seconds for notification for file modification to trigger
    timer.start(5000);
    eventLoop.exec();

    QCOMPARE(changedSpy.count(), 0);

    // readd the file watch with a relative path
    const QString relativeTestFileName = QDir::current().relativeFilePath(testFile.fileName());
    QVERIFY(!relativeTestFileName.isEmpty());
    QVERIFY(watcher.addPath(relativeTestFileName));
    testFile.open(QIODevice::WriteOnly | QIODevice::Truncate);
    testFile.write(QByteArray("hello multiverse!"));
    testFile.close();

    QTRY_VERIFY(changedSpy.count() > 0);

    QVERIFY(watcher.removePath(relativeTestFileName));

    changedSpy.clear();

    // readd the file watch
    QVERIFY(watcher.addPath(testFile.fileName()));

    // change the permissions, should get a signal from the watcher
    testFile.setPermissions(QFile::ReadOwner);

    // IN_ATTRIB doesn't work on QNX, so skip this test
#if !defined(Q_OS_QNX)

    // waiting max 5 seconds for notification for file permission modification to trigger
    QTRY_COMPARE(changedSpy.count(), 1);
    QCOMPARE(changedSpy.at(0).count(), 1);

    fileName = changedSpy.at(0).at(0).toString();
    QCOMPARE(fileName, testFile.fileName());

#endif

    changedSpy.clear();

    // remove the watch and modify file permissions, should not get a signal from the watcher
    QVERIFY(watcher.removePath(testFile.fileName()));
    testFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOther);

    // waiting max 5 seconds for notification for file modification to trigger
    timer.start(5000);
    eventLoop.exec();

    QCOMPARE(changedSpy.count(), 0);

    // readd the file watch
    QVERIFY(watcher.addPath(testFile.fileName()));

    // remove the file, should get a signal from the watcher
    QVERIFY(testFile.remove());

    // waiting max 5 seconds for notification for file removal to trigger
    // > 0 && < 3 because some platforms may emit two changes
    // XXX: which platforms? (QTBUG-23370)
    QTRY_VERIFY(changedSpy.count() > 0 && changedSpy.count() < 3);
    QCOMPARE(changedSpy.at(0).count(), 1);

    fileName = changedSpy.at(0).at(0).toString();
    QCOMPARE(fileName, testFile.fileName());

    changedSpy.clear();

    // recreate the file, we should not get any notification
    QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate));
    testFile.write(QByteArray("hello"));
    testFile.close();

    // waiting max 5 seconds for notification for file recreation to trigger
    timer.start(5000);
    eventLoop.exec();

    QCOMPARE(changedSpy.count(), 0);

    QVERIFY(testFile.remove());
}

void tst_QFileSystemWatcher::watchDirectory_data()
{
    QTest::addColumn<QString>("backend");
    QTest::addColumn<QStringList>("testDirNames");
    const QStringList testDirNames = {QStringLiteral("testdir"), QStringLiteral("testdir2")};

    QTest::newRow("native backend") << "native" << testDirNames;
    QTest::newRow("poller backend") << "poller" << testDirNames;
}

void tst_QFileSystemWatcher::watchDirectory()
{
    QFETCH(QString, backend);

    QTemporaryDir temporaryDirectory(m_tempDirPattern);
    QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));

    QFETCH(QStringList, testDirNames);

    QDir temporaryDir(temporaryDirectory.path());
    QStringList testDirs;
    QStringList testFiles;

    for (const auto &testDirName : testDirNames) {
        QVERIFY(temporaryDir.mkdir(testDirName));
        QDir testDir = temporaryDir;
        QVERIFY(testDir.cd(testDirName));

        testFiles.append(testDir.filePath("testFile.txt"));
        QFile::remove(testFiles.last());
        testDirs.append(testDir.absolutePath());
    }

    QFileSystemWatcher watcher;
    watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend);
    QVERIFY(watcher.addPaths(testDirs).isEmpty());

    QSignalSpy changedSpy(&watcher, &QFileSystemWatcher::directoryChanged);
    QVERIFY(changedSpy.isValid());
    QEventLoop eventLoop;
    QTimer timer;
    connect(&timer, SIGNAL(timeout()), &eventLoop, SLOT(quit()));

    // resolution of the modification time is system dependent, but it's at most 1 second when using
    // the polling engine. From what I know, FAT32 has a 2 second resolution. So we have to
    // wait before modifying the directory...
    QTest::qWait(2000);
    // remove the watch, should not get notification of a new file
    QVERIFY(watcher.removePaths(testDirs).isEmpty());
    for (const auto &testFileName : testFiles) {
        QFile testFile(testFileName);
        QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate));
        testFile.close();
    }

    // waiting max 5 seconds for notification for file recreationg to trigger
    timer.start(5000);
    eventLoop.exec();

    QCOMPARE(changedSpy.count(), 0);

    QVERIFY(watcher.addPaths(testDirs).isEmpty());

    // remove the file again, should get a signal from the watcher
    for (const auto &testFileName : testFiles)
        QVERIFY(QFile::remove(testFileName));

    timer.start(5000);
    eventLoop.exec();

    // remove the directory, should get a signal from the watcher
    for (const auto &testDirName : testDirs)
        QVERIFY(temporaryDir.rmdir(testDirName));

    QMap<QString, int> signalCounter;
    for (const auto &testDirName : testDirs)
        signalCounter[testDirName] = 0;

    // waiting max 5 seconds for notification for directory removal to trigger
    QTRY_COMPARE(changedSpy.count(), testDirs.size() * 2);
    for (int i = 0; i < changedSpy.count(); i++) {
        const auto &signal = changedSpy.at(i);
        QCOMPARE(signal.count(), 1);

        auto it = signalCounter.find(signal.at(0).toString());
        QVERIFY(it != signalCounter.end());
        QVERIFY(it.value() < 2);
        it.value()++;
    }

    for (const auto &count : signalCounter)
        QCOMPARE(count, 2);

    // flush pending signals (like the one from the rmdir above)
    timer.start(5000);
    eventLoop.exec();
    changedSpy.clear();

    // recreate the file, we should not get any notification
    for (const auto &testDirName : testDirNames) {
        if (!temporaryDir.mkdir(testDirName)) {
            QSKIP(qPrintable(QString::fromLatin1("Failed to recreate directory '%1' under '%2', skipping final test.").
                             arg(testDirName, temporaryDir.absolutePath())));
        }
    }

    // waiting max 5 seconds for notification for dir recreation to trigger
    timer.start(5000);
    eventLoop.exec();

    QCOMPARE(changedSpy.count(), 0);

    for (const auto &testDirName : testDirs)
        QVERIFY(temporaryDir.rmdir(testDirName));
}
#endif // QT_BUILD_INTERNAL

void tst_QFileSystemWatcher::addPath()
{
    QFileSystemWatcher watcher;
    QString home = QDir::homePath();
    QVERIFY(watcher.addPath(home));
    QCOMPARE(watcher.directories().count(), 1);
    QCOMPARE(watcher.directories().first(), home);

    // second watch on an already-watched path should fail
    QVERIFY(!watcher.addPath(home));
    QCOMPARE(watcher.directories().count(), 1);

    // With empty string
    QTest::ignoreMessage(QtWarningMsg, "QFileSystemWatcher::addPath: path is empty");
    QVERIFY(watcher.addPath(QString()));
}

void tst_QFileSystemWatcher::removePath()
{
    QFileSystemWatcher watcher;
    QString home = QDir::homePath();
    QVERIFY(watcher.addPath(home));
    QVERIFY(watcher.removePath(home));
    QCOMPARE(watcher.directories().count(), 0);
    QVERIFY(!watcher.removePath(home));
    QCOMPARE(watcher.directories().count(), 0);

    // With empty string
    QTest::ignoreMessage(QtWarningMsg, "QFileSystemWatcher::removePath: path is empty");
    QVERIFY(watcher.removePath(QString()));
}

void tst_QFileSystemWatcher::addPaths()
{
    QFileSystemWatcher watcher;
    QStringList paths;
    paths << QDir::homePath() << QDir::tempPath();
    QCOMPARE(watcher.addPaths(paths), QStringList());
    QCOMPARE(watcher.directories().count(), 2);

    // With empty list
    paths.clear();
    QTest::ignoreMessage(QtWarningMsg, "QFileSystemWatcher::addPaths: list is empty");
    QCOMPARE(watcher.addPaths(paths), QStringList());
}

// A signal spy that records the paths and times received for better diagnostics.
class FileSystemWatcherSpy : public QObject {
    Q_OBJECT
public:
    enum Mode {
        SpyOnDirectoryChanged,
        SpyOnFileChanged
    };

    explicit FileSystemWatcherSpy(QFileSystemWatcher *watcher, Mode mode)
    {
        connect(watcher, mode == SpyOnDirectoryChanged ?
                &QFileSystemWatcher::directoryChanged : &QFileSystemWatcher::fileChanged,
                this, &FileSystemWatcherSpy::spySlot);
        m_elapsedTimer.start();
    }

    int count() const { return m_entries.size(); }
    void clear()
    {
        m_entries.clear();
        m_elapsedTimer.restart();
    }

    QByteArray receivedFilesMessage() const
    {
        QString result;
        QTextStream str(&result);
        str << "At " << m_elapsedTimer.elapsed() << "ms, received "
            << count() << " changes: ";
        for (int i =0, e = m_entries.size(); i < e; ++i) {
            if (i)
                str << ", ";
            str << m_entries.at(i).timeStamp << "ms: " << QDir::toNativeSeparators(m_entries.at(i).path);
        }
        return result.toLocal8Bit();
    }

private slots:
    void spySlot(const QString &p) { m_entries.append(Entry(m_elapsedTimer.elapsed(), p)); }

private:
    struct Entry {
        Entry() : timeStamp(0) {}
        Entry(qint64 t, const QString &p) : timeStamp(t), path(p) {}

        qint64 timeStamp;
        QString path;
    };

    QElapsedTimer m_elapsedTimer;
    QList<Entry> m_entries;
};

void tst_QFileSystemWatcher::removePaths()
{
    QFileSystemWatcher watcher;
    QStringList paths;
    paths << QDir::homePath() << QDir::tempPath();
    QCOMPARE(watcher.addPaths(paths), QStringList());
    QCOMPARE(watcher.directories().count(), 2);
    QCOMPARE(watcher.removePaths(paths), QStringList());
    QCOMPARE(watcher.directories().count(), 0);

    //With empty list
    paths.clear();
    QTest::ignoreMessage(QtWarningMsg, "QFileSystemWatcher::removePaths: list is empty");
    watcher.removePaths(paths);
}

void tst_QFileSystemWatcher::removePathsFilesInSameDirectory()
{
    // QTBUG-46449/Windows: Check the return values of removePaths().
    // When adding the 1st file, a thread is started to watch the temp path.
    // After adding and removing the 2nd file, the thread is still running and
    // success should be reported.
    QTemporaryFile file1(m_tempDirPattern);
    QTemporaryFile file2(m_tempDirPattern);
    QVERIFY2(file1.open(), qPrintable(file1.errorString()));
    QVERIFY2(file2.open(), qPrintable(file1.errorString()));
    const QString path1 = file1.fileName();
    const QString path2 = file2.fileName();
    file1.close();
    file2.close();
    QFileSystemWatcher watcher;
    QVERIFY(watcher.addPath(path1));
    QCOMPARE(watcher.files().size(), 1);
    QVERIFY(watcher.addPath(path2));
    QCOMPARE(watcher.files().size(), 2);
    QVERIFY(watcher.removePath(path1));
    QCOMPARE(watcher.files().size(), 1);
    QVERIFY(watcher.removePath(path2));
    QCOMPARE(watcher.files().size(), 0);
}

#ifdef QT_BUILD_INTERNAL
static QByteArray msgFileOperationFailed(const char *what, const QFile &f)
{
    return what + QByteArrayLiteral(" failed on \"")
        + QDir::toNativeSeparators(f.fileName()).toLocal8Bit()
        + QByteArrayLiteral("\": ") + f.errorString().toLocal8Bit();
}

void tst_QFileSystemWatcher::watchFileAndItsDirectory()
{
    QFETCH(QString, backend);

    QTemporaryDir temporaryDirectory(m_tempDirPattern);
    QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));

    QDir temporaryDir(temporaryDirectory.path());
    const QString testDirName = QStringLiteral("testDir");
    QVERIFY(temporaryDir.mkdir(testDirName));
    QDir testDir = temporaryDir;
    QVERIFY(testDir.cd(testDirName));

    QString testFileName = testDir.filePath("testFile.txt");
    QString secondFileName = testDir.filePath("testFile2.txt");

    QFile testFile(testFileName);
    QVERIFY2(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate), msgFileOperationFailed("open", testFile));
    QVERIFY2(testFile.write(QByteArrayLiteral("hello")) > 0, msgFileOperationFailed("write", testFile));
    testFile.close();

    QFileSystemWatcher watcher;
    watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend);

    QVERIFY(watcher.addPath(testDir.absolutePath()));
    QVERIFY(watcher.addPath(testFileName));

    QSignalSpy fileChangedSpy(&watcher, &QFileSystemWatcher::fileChanged);
    FileSystemWatcherSpy dirChangedSpy(&watcher, FileSystemWatcherSpy::SpyOnDirectoryChanged);
    QVERIFY(fileChangedSpy.isValid());
    QEventLoop eventLoop;
    QTimer timer;
    connect(&timer, SIGNAL(timeout()), &eventLoop, SLOT(quit()));

    // resolution of the modification time is system dependent, but it's at most 1 second when using
    // the polling engine. From what I know, FAT32 has a 2 second resolution. So we have to
    // wait before modifying the directory...
    QTest::qWait(2000);

    QVERIFY2(testFile.open(QIODevice::WriteOnly | QIODevice::Truncate), msgFileOperationFailed("open", testFile));
    QVERIFY2(testFile.write(QByteArrayLiteral("hello again")), msgFileOperationFailed("write", testFile));
    testFile.close();

#ifdef Q_OS_MAC
    // wait again for the file's atime to be updated
    QTest::qWait(2000);
#endif

    QTRY_VERIFY(fileChangedSpy.count() > 0);
    QVERIFY2(dirChangedSpy.count() == 0, dirChangedSpy.receivedFilesMessage());

    fileChangedSpy.clear();
    QFile secondFile(secondFileName);
    QVERIFY2(secondFile.open(QIODevice::WriteOnly | QIODevice::Truncate), msgFileOperationFailed("open", secondFile));
    QVERIFY2(secondFile.write(QByteArrayLiteral("Foo")) > 0, msgFileOperationFailed("write", secondFile));
    secondFile.close();

    timer.start(3000);
    eventLoop.exec();
    int fileChangedSpyCount = fileChangedSpy.count();
#ifdef Q_OS_WIN
    if (fileChangedSpyCount != 0)
        QEXPECT_FAIL("", "See QTBUG-30943", Continue);
#endif
    QCOMPARE(fileChangedSpyCount, 0);
    QCOMPARE(dirChangedSpy.count(), 1);

    dirChangedSpy.clear();

    QVERIFY(QFile::remove(testFileName));

    QTRY_VERIFY(fileChangedSpy.count() > 0);
    QTRY_COMPARE(dirChangedSpy.count(), 1);

    fileChangedSpy.clear();
    dirChangedSpy.clear();

    // removing a deleted file should fail
    QVERIFY(!watcher.removePath(testFileName));
    QVERIFY(QFile::remove(secondFileName));

    timer.start(3000);
    eventLoop.exec();
    QCOMPARE(fileChangedSpy.count(), 0);
    QCOMPARE(dirChangedSpy.count(), 1);

    // QTBUG-61792, removal should succeed (bug on Windows which uses one change
    // notification per directory).
    QVERIFY(watcher.removePath(testDir.absolutePath()));

    QVERIFY(temporaryDir.rmdir(testDirName));
}
#endif // QT_BUILD_INTERNAL

void tst_QFileSystemWatcher::nonExistingFile()
{
    // Don't crash...
    QFileSystemWatcher watcher;
    QVERIFY(!watcher.addPath("file_that_does_not_exist.txt"));

    // Test that the paths returned in error aren't messed with
    QCOMPARE(watcher.addPaths(QStringList() << "../..//./does-not-exist"),
                              QStringList() << "../..//./does-not-exist");

    // empty path is not actually a failure
    QCOMPARE(watcher.addPaths(QStringList() << QString()), QStringList());

    // empty path is not actually a failure
    QCOMPARE(watcher.removePaths(QStringList() << QString()), QStringList());
}

void tst_QFileSystemWatcher::removeFileAndUnWatch()
{
    QTemporaryDir temporaryDirectory(m_tempDirPattern);
    QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));

    const QString filename = temporaryDirectory.path() + QStringLiteral("/foo.txt");

    QFileSystemWatcher watcher;

    {
        QFile testFile(filename);
        QVERIFY2(testFile.open(QIODevice::WriteOnly),
                 qPrintable(QString::fromLatin1("Cannot open %1 for writing: %2").arg(filename, testFile.errorString())));
        testFile.close();
    }
    QVERIFY(watcher.addPath(filename));

    QFile::remove(filename);
    /* There are potential race conditions here; the watcher thread might remove the file from its list
     * before the call to watcher.removePath(), which then fails. When that happens, the auto-signal
     * notification to remove the file from the watcher's main list will not be delivered before the next
     * event loop such that the call to watcher.addPath() fails since the file is still in the main list. */
    if (!watcher.removePath(filename))
        QSKIP("Skipping remaining test due to race condition.");

    {
        QFile testFile(filename);
        QVERIFY2(testFile.open(QIODevice::WriteOnly),
                 qPrintable(QString::fromLatin1("Cannot open %1 for writing: %2").arg(filename, testFile.errorString())));
        testFile.close();
    }
    QVERIFY(watcher.addPath(filename));
}

class SomeSingleton : public QObject
{
public:
    SomeSingleton() : mFsWatcher(new QFileSystemWatcher(this)) { mFsWatcher->addPath(QLatin1String("/usr/lib"));}
    void bla() const {}
    QFileSystemWatcher* mFsWatcher;
};

Q_GLOBAL_STATIC(SomeSingleton, someSingleton)

// This is a regression test for QTBUG-15255, where a deadlock occurred if a
// QFileSystemWatcher was destroyed after the QCoreApplication instance had
// been destroyed.  There are no explicit verification steps in this test --
// it is sufficient that the test terminates.
void tst_QFileSystemWatcher::destroyAfterQCoreApplication()
{
    someSingleton()->bla();
    QTest::qWait(30);
}

#ifdef QT_BUILD_INTERNAL
// regression test for QTBUG2331.
// essentially, on windows, directories were not unwatched after being deleted
// from the disk, causing all sorts of interesting problems.
void tst_QFileSystemWatcher::QTBUG2331()
{
    QFETCH(QString, backend);

    QTemporaryDir temporaryDirectory(m_tempDirPattern);
    QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));
    QFileSystemWatcher watcher;
    watcher.setObjectName(QLatin1String("_qt_autotest_force_engine_") + backend);
    QVERIFY(watcher.addPath(temporaryDirectory.path()));

    // watch signal
    QSignalSpy changedSpy(&watcher, &QFileSystemWatcher::directoryChanged);
    QVERIFY(changedSpy.isValid());

    // remove directory, we should get one change signal, and we should no longer
    // be watching the directory.
    QVERIFY(temporaryDirectory.remove());
    QTRY_COMPARE(changedSpy.count(), 1);
    QCOMPARE(watcher.directories(), QStringList());
}
#endif // QT_BUILD_INTERNAL

class SignalReceiver : public QObject
{
    Q_OBJECT
public:
    SignalReceiver(const QDir &moveSrcDir,
                   const QString &moveDestination,
                   QFileSystemWatcher *watcher,
                   QObject *parent = nullptr)
        : QObject(parent),
          added(false),
          moveSrcDir(moveSrcDir),
          moveDestination(QDir(moveDestination)),
          watcher(watcher)
    {}

public slots:
    void fileChanged(const QString &path)
    {
        QFileInfo finfo(path);

        QCOMPARE(finfo.absolutePath(), moveSrcDir.absolutePath());

        if (!added) {
            foreach (const QFileInfo &fi, moveDestination.entryInfoList(QDir::Files | QDir::NoSymLinks))
                watcher->addPath(fi.absoluteFilePath());
            added = true;
        }
    }

private:
    bool added;
    QDir moveSrcDir;
    QDir moveDestination;
    QFileSystemWatcher *watcher;
};

// regression test for QTBUG-33211.
// using inotify backend if a file is moved and then added to the watcher
// before all the fileChanged signals are emitted the remaining signals are
// emitted with the destination path instead of the starting path
void tst_QFileSystemWatcher::signalsEmittedAfterFileMoved()
{
    const int fileCount = 10;
    QTemporaryDir temporaryDirectory(m_tempDirPattern);
    QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));

    QDir testDir(temporaryDirectory.path());
    QVERIFY(testDir.mkdir("movehere"));
    QString movePath = testDir.filePath("movehere");

    for (int i = 0; i < fileCount; ++i) {
        const QByteArray iB = QByteArray::number(i);
        QFile f(testDir.filePath(QLatin1String("test") + QString::fromLatin1(iB) + QLatin1String(".txt")));
        QVERIFY(f.open(QIODevice::WriteOnly));
        f.write(QByteArray("i am ") + iB);
        f.close();
    }

    QFileSystemWatcher watcher;
    QVERIFY(watcher.addPath(testDir.path()));
    QVERIFY(watcher.addPath(movePath));

    // add files to watcher
    QFileInfoList files = testDir.entryInfoList(QDir::Files | QDir::NoSymLinks);
    QCOMPARE(files.size(), fileCount);
    foreach (const QFileInfo &finfo, files)
        QVERIFY(watcher.addPath(finfo.absoluteFilePath()));

    // create the signal receiver
    SignalReceiver signalReceiver(testDir, movePath, &watcher);
    connect(&watcher, SIGNAL(fileChanged(QString)), &signalReceiver, SLOT(fileChanged(QString)));

    // watch signals
    FileSystemWatcherSpy changedSpy(&watcher, FileSystemWatcherSpy::SpyOnFileChanged);
    QCOMPARE(changedSpy.count(), 0);

    // move files to second directory
    foreach (const QFileInfo &finfo, files)
        QVERIFY(testDir.rename(finfo.fileName(), QString("movehere/%2").arg(finfo.fileName())));

    QCoreApplication::processEvents();
    QVERIFY2(changedSpy.count() <= fileCount, changedSpy.receivedFilesMessage());
    QTRY_COMPARE(changedSpy.count(), fileCount);
}

void tst_QFileSystemWatcher::watchUnicodeCharacters()
{
    QTemporaryDir temporaryDirectory(m_tempDirPattern);
    QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));

    QDir testDir(temporaryDirectory.path());
    const QString subDir(QString::fromLatin1("caf\xe9"));
    QVERIFY(testDir.mkdir(subDir));
    testDir = QDir(temporaryDirectory.path() + QDir::separator() + subDir);

    QFileSystemWatcher watcher;
    QVERIFY(watcher.addPath(testDir.path()));

    FileSystemWatcherSpy changedSpy(&watcher, FileSystemWatcherSpy::SpyOnDirectoryChanged);
    QCOMPARE(changedSpy.count(), 0);
    QVERIFY(testDir.mkdir("creme"));
    QTRY_COMPARE(changedSpy.count(), 1);
}

#if defined(Q_OS_WIN)
void tst_QFileSystemWatcher::watchDirectoryAttributeChanges()
{
    QTemporaryDir temporaryDirectory(m_tempDirPattern);
    QVERIFY2(temporaryDirectory.isValid(), qPrintable(temporaryDirectory.errorString()));

    QDir testDir(temporaryDirectory.path());
    const QString subDir(QString::fromLatin1("attrib_test"));
    QVERIFY(testDir.mkdir(subDir));
    testDir = QDir(temporaryDirectory.path() + QDir::separator() + subDir);

    QFileSystemWatcher watcher;
    QVERIFY(watcher.addPath(temporaryDirectory.path()));
    FileSystemWatcherSpy changedSpy(&watcher, FileSystemWatcherSpy::SpyOnDirectoryChanged);
    QCOMPARE(changedSpy.count(), 0);
    QVERIFY(SetFileAttributes(reinterpret_cast<LPCWSTR>(testDir.absolutePath().utf16()), FILE_ATTRIBUTE_HIDDEN) != 0);
    QTRY_COMPARE(changedSpy.count(), 1);
    QVERIFY(SetFileAttributes(reinterpret_cast<LPCWSTR>(testDir.absolutePath().utf16()), FILE_ATTRIBUTE_NORMAL) != 0);
    QTRY_COMPARE(changedSpy.count(), 2);
}
#endif

QTEST_MAIN(tst_QFileSystemWatcher)
#include "tst_qfilesystemwatcher.moc"
