// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include <QtCore/QCoreApplication>
#include <QtCore/QEvent>
#include <QtCore/QMutex>
#include <QtCore/QObject>
#include <QtCore/QTimer>
#include <QtGui/private/qwasmlocalfileaccess_p.h>

#include <qtwasmtestlib.h>
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>

#include <string_view>

using namespace emscripten;

class FilesTest : public QObject
{
    Q_OBJECT

public:
    FilesTest() : m_window(val::global("window")), m_testSupport(val::object()) {}

    ~FilesTest() noexcept {
        for (auto& cleanup: m_cleanup) {
            cleanup();
        }
    }

private:
    void init() {
        EM_ASM({
            window.testSupport = {};

            window.showOpenFilePicker = sinon.stub();

            window.mockOpenFileDialog = (files) => {
                window.showOpenFilePicker.withArgs(sinon.match.any).callsFake(
                    (options) => Promise.resolve(files.map(file => {
                        const getFile = sinon.stub();
                        getFile.callsFake(() => Promise.resolve({
                            name: file.name,
                            size: file.content.length,
                            slice: () => new Blob([new TextEncoder().encode(file.content)]),
                        }));
                        return {
                            kind: 'file',
                            name: file.name,
                            getFile
                        };
                    }))
                );
            };

            window.showSaveFilePicker = sinon.stub();

            window.mockSaveFilePicker = (file) => {
                window.showSaveFilePicker.withArgs(sinon.match.any).callsFake(
                    (options) => {
                        const createWritable = sinon.stub();
                        createWritable.callsFake(() => {
                            const write = file.writeFn ?? (() => {
                                const write = sinon.stub();
                                write.callsFake((stuff) => {
                                    if (file.content !== new TextDecoder().decode(stuff)) {
                                        const message = `Bad file content ${file.content} !== ${new TextDecoder().decode(stuff)}`;
                                        Module.qtWasmFail(message);
                                        return Promise.reject(message);
                                    }

                                    return Promise.resolve();
                                });
                                return write;
                            })();

                            window.testSupport.write = write;

                            const close = file.closeFn ?? (() => {
                                const close = sinon.stub();
                                close.callsFake(() => Promise.resolve());
                                return close;
                            })();

                            window.testSupport.close = close;

                            return Promise.resolve({
                                write,
                                close
                            });
                        });
                        return Promise.resolve({
                            kind: 'file',
                            name: file.name,
                            createWritable
                        });
                    }
                );
            };
        });
    }

    template <class T>
    T* Own(T* plainPtr) {
        m_cleanup.emplace_back([plainPtr]() mutable {
            delete plainPtr;
        });
        return plainPtr;
    }

    val m_window;
    val m_testSupport;

    std::vector<std::function<void()>> m_cleanup;

private slots:
    void selectOneFileWithFileDialog();
    void selectMultipleFilesWithFileDialog();
    void cancelFileDialog();
    void rejectFile();
    void saveFileWithFileDialog();
};

class BarrierCallback {
public:
    BarrierCallback(int number, std::function<void()> onDone)
        : m_remaining(number), m_onDone(std::move(onDone)) {}

    void operator()() {
        if (!--m_remaining) {
            m_onDone();
        }
    }

private:
    int m_remaining;
    std::function<void()> m_onDone;
};


template <class Arg>
std::string argToString(std::add_lvalue_reference_t<std::add_const_t<Arg>> arg) {
    return std::to_string(arg);
}

template <>
std::string argToString<bool>(const bool& value) {
    return value ? "true" : "false";
}

template <>
std::string argToString<std::string>(const std::string& arg) {
    return arg;
}

template <>
std::string argToString<const std::string&>(const std::string& arg) {
    return arg;
}

template<class Type>
struct Matcher {
    virtual ~Matcher() = default;

    virtual bool matches(std::string* explanation, const Type& actual) const = 0;
};

template<class Type>
struct AnyMatcher : public Matcher<Type> {
    bool matches(std::string* explanation, const Type& actual) const final {
        Q_UNUSED(explanation);
        Q_UNUSED(actual);
        return true;
    }

    Type m_value;
};

template<class Type>
struct EqualsMatcher : public Matcher<Type> {
    EqualsMatcher(Type value) : m_value(std::forward<Type>(value)) {}

    bool matches(std::string* explanation, const Type& actual) const final {
        const bool ret = actual == m_value;
        if (!ret)
            *explanation += argToString<Type>(actual) + " != " + argToString<Type>(m_value);
        return actual == m_value;
    }

    // It is crucial to hold a copy, otherwise we lose const refs.
    std::remove_reference_t<Type> m_value;
};

template<class Type>
std::unique_ptr<EqualsMatcher<Type>> equals(Type value) {
    return std::make_unique<EqualsMatcher<Type>>(value);
}

template<class Type>
std::unique_ptr<AnyMatcher<Type>> any(Type value) {
    return std::make_unique<AnyMatcher<Type>>(value);
}

template <class ...Types>
struct Expectation {
    std::tuple<std::unique_ptr<Matcher<Types>>...> m_argMatchers;
    int m_callCount = 0;
    int m_expectedCalls = 1;

    template<std::size_t... Indices>
    bool match(std::string* explanation, const std::tuple<Types...>& tuple, std::index_sequence<Indices...>) const {
        return ( ... && (std::get<Indices>(m_argMatchers)->matches(explanation, std::get<Indices>(tuple))));
    }

    bool matches(std::string* explanation, Types... args) const {
        if (m_callCount >= m_expectedCalls) {
            *explanation += "Too many calls\n";
            return false;
        }
        return match(explanation, std::make_tuple(args...), std::make_index_sequence<std::tuple_size_v<std::tuple<Types...>>>());
    }
};

template <class R, class ...Types>
struct Behavior {
    std::function<R(Types...)> m_callback;

    void call(std::function<R(Types...)> callback) {
        m_callback = std::move(callback);
    }
};

template<class... Args>
std::string argsToString(Args... args) {
    return (... + (", " + argToString<Args>(args)));
}

template<>
std::string argsToString<>() {
    return "";
}

template<class R, class ...Types>
struct ExpectationToBehaviorMapping {
    Expectation<Types...> expectation;
    Behavior<R, Types...> behavior;
};

template<class R, class... Args>
class MockCallback {
public:
    std::function<R(Args...)> get() {
        return [this](Args... result) -> R {
            return processCall(std::forward<Args>(result)...);
        };
    }

    Behavior<R, Args...>& expectCallWith(std::unique_ptr<Matcher<Args>>... matcherArgs) {
        auto matchers = std::make_tuple(std::move(matcherArgs)...);
        m_behaviorByExpectation.push_back({Expectation<Args...> {std::move(matchers)}, Behavior<R, Args...> {}});
        return m_behaviorByExpectation.back().behavior;
    }

    Behavior<R, Args...>& expectRepeatedCallWith(int times, std::unique_ptr<Matcher<Args>>... matcherArgs) {
        auto matchers = std::make_tuple(std::move(matcherArgs)...);
        m_behaviorByExpectation.push_back({Expectation<Args...> {std::move(matchers), 0, times}, Behavior<R, Args...> {}});
        return m_behaviorByExpectation.back().behavior;
    }

private:
    R processCall(Args... args) {
        std::string argsAsString = argsToString(args...);
        std::string triedExpectations;
        auto it = std::find_if(m_behaviorByExpectation.begin(), m_behaviorByExpectation.end(),
            [&](const ExpectationToBehaviorMapping<R, Args...>& behavior) {
                return behavior.expectation.matches(&triedExpectations, std::forward<Args>(args)...);
            });
        if (it != m_behaviorByExpectation.end()) {
            ++it->expectation.m_callCount;
            return it->behavior.m_callback(args...);
        } else {
            QWASMFAIL("Unexpected call with " + argsAsString + ". Tried: " + triedExpectations);
        }
        return R();
    }

    std::vector<ExpectationToBehaviorMapping<R, Args...>> m_behaviorByExpectation;
};

void FilesTest::selectOneFileWithFileDialog()
{
    init();

    static constexpr std::string_view testFileContent = "This is a happy case.";

    EM_ASM({
        mockOpenFileDialog([{
            name: 'file1.jpg',
            content: UTF8ToString($0)
        }]);
    }, testFileContent.data());

    auto* fileSelectedCallback = Own(new MockCallback<void, bool>());
    fileSelectedCallback->expectCallWith(equals(true)).call([](bool) mutable {});

    auto* fileBuffer = Own(new QByteArray());

    auto* acceptFileCallback = Own(new MockCallback<char*, uint64_t, const std::string&>());
    acceptFileCallback->expectCallWith(equals<uint64_t>(testFileContent.size()), equals<const std::string&>("file1.jpg"))
        .call([fileBuffer](uint64_t, std::string) mutable -> char* {
            fileBuffer->resize(testFileContent.size());
            return fileBuffer->data();
        });

    auto* fileDataReadyCallback = Own(new MockCallback<void>());
    fileDataReadyCallback->expectCallWith().call([fileBuffer]() mutable {
        QWASMCOMPARE(fileBuffer->data(), std::string(testFileContent));
        QWASMSUCCESS();
    });

    QWasmLocalFileAccess::openFile(
        {QStringLiteral("*")}, fileSelectedCallback->get(), acceptFileCallback->get(), fileDataReadyCallback->get());
}

void FilesTest::selectMultipleFilesWithFileDialog()
{
    static constexpr std::array<std::string_view, 3> testFileContent =
        { "Cont 1", "2s content", "What is hiding in 3?"};

    init();

    EM_ASM({
        mockOpenFileDialog([{
            name: 'file1.jpg',
            content: UTF8ToString($0)
        }, {
            name: 'file2.jpg',
            content: UTF8ToString($1)
        }, {
            name: 'file3.jpg',
            content: UTF8ToString($2)
        }]);
    }, testFileContent[0].data(), testFileContent[1].data(), testFileContent[2].data());

    auto* fileSelectedCallback = Own(new MockCallback<void, int>());
    fileSelectedCallback->expectCallWith(equals(3)).call([](int) mutable {});

    auto fileBuffer = std::make_shared<QByteArray>();

    auto* acceptFileCallback = Own(new MockCallback<char*, uint64_t, const std::string&>());
    acceptFileCallback->expectCallWith(equals<uint64_t>(testFileContent[0].size()), equals<const std::string&>("file1.jpg"))
        .call([fileBuffer](uint64_t, std::string) mutable -> char* {
            fileBuffer->resize(testFileContent[0].size());
            return fileBuffer->data();
        });
    acceptFileCallback->expectCallWith(equals<uint64_t>(testFileContent[1].size()), equals<const std::string&>("file2.jpg"))
        .call([fileBuffer](uint64_t, std::string) mutable -> char* {
            fileBuffer->resize(testFileContent[1].size());
            return fileBuffer->data();
        });
    acceptFileCallback->expectCallWith(equals<uint64_t>(testFileContent[2].size()), equals<const std::string&>("file3.jpg"))
        .call([fileBuffer](uint64_t, std::string) mutable -> char* {
            fileBuffer->resize(testFileContent[2].size());
            return fileBuffer->data();
        });

    auto* fileDataReadyCallback = Own(new MockCallback<void>());
    fileDataReadyCallback->expectRepeatedCallWith(3).call([fileBuffer]() mutable {
        static int callCount = 0;
        QWASMCOMPARE(fileBuffer->data(), std::string(testFileContent[callCount]));

        callCount++;
        if (callCount == 3) {
            QWASMSUCCESS();
        }
    });

    QWasmLocalFileAccess::openFiles(
        {QStringLiteral("*")}, QWasmLocalFileAccess::FileSelectMode::MultipleFiles,
        fileSelectedCallback->get(), acceptFileCallback->get(), fileDataReadyCallback->get());
}

void FilesTest::cancelFileDialog()
{
    init();

    EM_ASM({
        window.showOpenFilePicker.withArgs(sinon.match.any).returns(Promise.reject("The user cancelled the dialog"));
    });

    auto* fileSelectedCallback = Own(new MockCallback<void, bool>());
    fileSelectedCallback->expectCallWith(equals(false)).call([](bool) mutable {
        QWASMSUCCESS();
    });

    auto* acceptFileCallback = Own(new MockCallback<char*, uint64_t, const std::string&>());
    auto* fileDataReadyCallback = Own(new MockCallback<void>());

    QWasmLocalFileAccess::openFile(
        {QStringLiteral("*")}, fileSelectedCallback->get(), acceptFileCallback->get(), fileDataReadyCallback->get());
}

void FilesTest::rejectFile()
{
    init();

    static constexpr std::string_view testFileContent = "We don't want this file.";

    EM_ASM({
        mockOpenFileDialog([{
            name: 'dontwant.dat',
            content: UTF8ToString($0)
        }]);
    }, testFileContent.data());

    auto* fileSelectedCallback = Own(new MockCallback<void, bool>());
    fileSelectedCallback->expectCallWith(equals(true)).call([](bool) mutable {});

    auto* fileDataReadyCallback = Own(new MockCallback<void>());

    auto* acceptFileCallback = Own(new MockCallback<char*, uint64_t, const std::string&>());
    acceptFileCallback->expectCallWith(equals<uint64_t>(std::string_view(testFileContent).size()), equals<const std::string&>("dontwant.dat"))
        .call([](uint64_t, const std::string) {
            QTimer::singleShot(0, []() {
                // No calls to fileDataReadyCallback
                QWASMSUCCESS();
            });
            return nullptr;
        });

    QWasmLocalFileAccess::openFile(
        {QStringLiteral("*")}, fileSelectedCallback->get(), acceptFileCallback->get(), fileDataReadyCallback->get());
}

void FilesTest::saveFileWithFileDialog()
{
    init();

    static constexpr std::string_view testFileContent = "Save this important content";

    EM_ASM({
        mockSaveFilePicker({
            name: 'somename',
            content: UTF8ToString($0),
            closeFn: (() => {
                const close = sinon.stub();
                close.callsFake(() =>
                    new Promise(resolve => {
                        resolve();
                        Module.qtWasmPass();
                    }));
                return close;
            })()
        });
    }, testFileContent.data());

    QByteArray data;
    data.prepend(testFileContent);
    QWasmLocalFileAccess::saveFile(data, "hintie");
}

int main(int argc, char **argv)
{
    auto testObject = std::make_shared<FilesTest>();
    QtWasmTest::initTestCase<QCoreApplication>(argc, argv, testObject);
    return 0;
}

#include "files_main.moc"