/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"

#include "Test.h"
#include "Utilities.h"
#include "WTFStringUtilities.h"
#include <WebCore/FileMonitor.h>
#include <wtf/FileSystem.h>
#include <wtf/MainThread.h>
#include <wtf/RunLoop.h>
#include <wtf/Scope.h>
#include <wtf/StringExtras.h>
#include <wtf/WorkQueue.h>
#include <wtf/text/StringBuffer.h>

// Note: Disabling iOS since 'system' is not available on that platform.
#if PLATFORM(MAC) || PLATFORM(GTK) || PLATFORM(WPE)

using namespace WebCore;

namespace TestWebKitAPI {
    
const String FileMonitorTestData("This is a test"_s);
const String FileMonitorRevisedData("This is some changed text for the test"_s);
const String FileMonitorSecondRevisedData("This is some changed text for the test"_s);

class FileMonitorTest : public testing::Test {
public:
    void SetUp() override
    {
        WTF::initializeMainThread();
        
        // create temp file
        FileSystem::PlatformFileHandle handle;
        m_tempFilePath = FileSystem::openTemporaryFile("tempTestFile"_s, handle);
        ASSERT_NE(handle, FileSystem::invalidPlatformFileHandle);
        
        int rc = FileSystem::writeToFile(handle, FileMonitorTestData.utf8().data(), FileMonitorTestData.length());
        ASSERT_NE(rc, -1);
        
        FileSystem::closeFile(handle);
    }
    
    void TearDown() override
    {
        FileSystem::deleteFile(m_tempFilePath);
    }
    
    const String& tempFilePath() { return m_tempFilePath; }
    
private:
    String m_tempFilePath;
};
    
static bool observedFileModification = false;
static bool observedFileDeletion = false;
static bool didFinish = false;
    
static void handleFileModification()
{
    observedFileModification = true;
    didFinish = true;
}
    
static void handleFileDeletion()
{
    observedFileDeletion = true;
    didFinish = true;
}
    
static void resetTestState()
{
    observedFileModification = false;
    observedFileDeletion = false;
    didFinish = false;
}

static String createCommand(const String& path, const String& payload)
{
    return makeString("echo \"", payload, "\" > ", path);
}

static String readContentsOfFile(const String& path)
{
    auto buffer = FileSystem::readEntireFile(path);
    if (!buffer)
        return emptyString();

    String result(static_cast<const LChar*>(buffer->data()), buffer->size());
    if (result.endsWith('\n'))
        return result.left(result.length() - 1);

    return result;
}

TEST_F(FileMonitorTest, DetectChange)
{
    EXPECT_TRUE(FileSystem::fileExists(tempFilePath()));

    WTF::initializeMainThread();

    auto testQueue = WorkQueue::create("Test Work Queue");

    auto monitor = makeUnique<FileMonitor>(tempFilePath(), testQueue.copyRef(), [] (FileMonitor::FileChangeType type) {
        ASSERT(!RunLoop::isMain());
        switch (type) {
        case FileMonitor::FileChangeType::Modification:
            handleFileModification();
            break;
        case FileMonitor::FileChangeType::Removal:
            handleFileDeletion();
            break;
        }
    });

    testQueue->dispatch([this] () mutable {
        String fileContents = readContentsOfFile(tempFilePath());
        EXPECT_STREQ(FileMonitorTestData.utf8().data(), fileContents.utf8().data());

        auto command = createCommand(tempFilePath(), FileMonitorRevisedData);
        auto rc = system(command.utf8().data());
        ASSERT_NE(rc, -1);
        if (rc == -1)
            didFinish = true;
    });

    Util::run(&didFinish);

    EXPECT_TRUE(observedFileModification);
    EXPECT_FALSE(observedFileDeletion);

    String revisedFileContents = readContentsOfFile(tempFilePath());
    EXPECT_STREQ(FileMonitorRevisedData.utf8().data(), revisedFileContents.utf8().data());

    resetTestState();
}

TEST_F(FileMonitorTest, DetectMultipleChanges)
{
    EXPECT_TRUE(FileSystem::fileExists(tempFilePath()));

    WTF::initializeMainThread();

    auto testQueue = WorkQueue::create("Test Work Queue");

    auto monitor = makeUnique<FileMonitor>(tempFilePath(), testQueue.copyRef(), [] (FileMonitor::FileChangeType type) {
        ASSERT(!RunLoop::isMain());
        switch (type) {
        case FileMonitor::FileChangeType::Modification:
            handleFileModification();
            break;
        case FileMonitor::FileChangeType::Removal:
            handleFileDeletion();
            break;
        }
    });
    
    testQueue->dispatch([this] () mutable {
        String fileContents = readContentsOfFile(tempFilePath());
        EXPECT_STREQ(FileMonitorTestData.utf8().data(), fileContents.utf8().data());

        auto firstCommand = createCommand(tempFilePath(), FileMonitorRevisedData);
        auto rc = system(firstCommand.utf8().data());
        ASSERT_NE(rc, -1);
        if (rc == -1)
            didFinish = true;
    });

    Util::run(&didFinish);

    EXPECT_TRUE(observedFileModification);
    EXPECT_FALSE(observedFileDeletion);

    String revisedFileContents = readContentsOfFile(tempFilePath());
    EXPECT_STREQ(FileMonitorRevisedData.utf8().data(), revisedFileContents.utf8().data());

    resetTestState();

    testQueue->dispatch([this] () mutable {
        auto secondCommand = createCommand(tempFilePath(), FileMonitorSecondRevisedData);
        auto rc = system(secondCommand.utf8().data());
        ASSERT_NE(rc, -1);
        if (rc == -1)
            didFinish = true;
    });

    Util::run(&didFinish);

    EXPECT_TRUE(observedFileModification);
    EXPECT_FALSE(observedFileDeletion);

    String secondRevisedfileContents = readContentsOfFile(tempFilePath());
    EXPECT_STREQ(FileMonitorSecondRevisedData.utf8().data(), secondRevisedfileContents.utf8().data());

    resetTestState();
}

TEST_F(FileMonitorTest, DetectDeletion)
{
    EXPECT_TRUE(FileSystem::fileExists(tempFilePath()));

    WTF::initializeMainThread();

    auto testQueue = WorkQueue::create("Test Work Queue");

    auto monitor = makeUnique<FileMonitor>(tempFilePath(), testQueue.copyRef(), [] (FileMonitor::FileChangeType type) {
        ASSERT(!RunLoop::isMain());
        switch (type) {
        case FileMonitor::FileChangeType::Modification:
            handleFileModification();
            break;
        case FileMonitor::FileChangeType::Removal:
            handleFileDeletion();
            break;
        }
    });

    testQueue->dispatch([this] () mutable {
        auto rc = system(makeString("rm -f ", tempFilePath()).utf8().data());
        ASSERT_NE(rc, -1);
        if (rc == -1)
            didFinish = true;
    });

    Util::run(&didFinish);

    EXPECT_FALSE(observedFileModification);
    EXPECT_TRUE(observedFileDeletion);

    resetTestState();
}

TEST_F(FileMonitorTest, DetectChangeAndThenDelete)
{
    EXPECT_TRUE(FileSystem::fileExists(tempFilePath()));

    WTF::initializeMainThread();

    auto testQueue = WorkQueue::create("Test Work Queue");

    auto monitor = makeUnique<FileMonitor>(tempFilePath(), testQueue.copyRef(), [] (FileMonitor::FileChangeType type) {
        ASSERT(!RunLoop::isMain());
        switch (type) {
            case FileMonitor::FileChangeType::Modification:
                handleFileModification();
                break;
            case FileMonitor::FileChangeType::Removal:
                handleFileDeletion();
                break;
        }
    });

    testQueue->dispatch([this] () mutable {
        String fileContents = readContentsOfFile(tempFilePath());
        EXPECT_STREQ(FileMonitorTestData.utf8().data(), fileContents.utf8().data());

        auto firstCommand = createCommand(tempFilePath(), FileMonitorRevisedData);
        auto rc = system(firstCommand.utf8().data());
        ASSERT_NE(rc, -1);
        if (rc == -1)
            didFinish = true;
    });

    Util::run(&didFinish);

    EXPECT_TRUE(observedFileModification);
    EXPECT_FALSE(observedFileDeletion);

    resetTestState();

    testQueue->dispatch([this] () mutable {
        auto rc = system(makeString("rm -f ", tempFilePath()).utf8().data());
        ASSERT_NE(rc, -1);
        if (rc == -1)
            didFinish = true;
    });

    Util::run(&didFinish);

    EXPECT_FALSE(observedFileModification);
    EXPECT_TRUE(observedFileDeletion);

    resetTestState();
}

TEST_F(FileMonitorTest, DetectDeleteButNotSubsequentChange)
{
    EXPECT_TRUE(FileSystem::fileExists(tempFilePath()));

    WTF::initializeMainThread();

    auto testQueue = WorkQueue::create("Test Work Queue");

    auto monitor = makeUnique<FileMonitor>(tempFilePath(), testQueue.copyRef(), [] (FileMonitor::FileChangeType type) {
        ASSERT(!RunLoop::isMain());
        switch (type) {
            case FileMonitor::FileChangeType::Modification:
                handleFileModification();
                break;
            case FileMonitor::FileChangeType::Removal:
                handleFileDeletion();
                break;
        }
    });

    testQueue->dispatch([this] () mutable {
        auto rc = system(makeString("rm -f ", tempFilePath()).utf8().data());
        ASSERT_NE(rc, -1);
        if (rc == -1)
            didFinish = true;
    });

    Util::run(&didFinish);

    EXPECT_FALSE(observedFileModification);
    EXPECT_TRUE(observedFileDeletion);

    resetTestState();

    testQueue->dispatch([this] () mutable {
        EXPECT_FALSE(FileSystem::fileExists(tempFilePath()));

        auto handle = FileSystem::openFile(tempFilePath(), FileSystem::FileOpenMode::Write);
        ASSERT_NE(handle, FileSystem::invalidPlatformFileHandle);

        int rc = FileSystem::writeToFile(handle, FileMonitorTestData.utf8().data(), FileMonitorTestData.length());
        ASSERT_NE(rc, -1);

        auto firstCommand = createCommand(tempFilePath(), FileMonitorRevisedData);
        rc = system(firstCommand.utf8().data());
        ASSERT_NE(rc, -1);
        if (rc == -1)
            didFinish = true;
    });

    // Set a timer to end the test, since we do not expect the file modification
    // to be observed.
    testQueue->dispatchAfter(500_ms, [&](void) {
        EXPECT_FALSE(observedFileModification);
        EXPECT_FALSE(observedFileDeletion);
        didFinish = true;
    });

    Util::run(&didFinish);

    EXPECT_FALSE(observedFileModification);
    EXPECT_FALSE(observedFileDeletion);

    resetTestState();
}

}

#endif

