#include <QTest>

#include <QString>
#include <QSharedPointer>

#include "store.h"
#include "commands.h"
#include "entitybuffer.h"
#include "pipeline.h"
#include "synchronizer.h"
#include "commandprocessor.h"
#include "definitions.h"
#include "adaptorfactoryregistry.h"
#include "datastorequery.h"
#include "genericresource.h"
#include "test.h"

class TestSynchronizer: public Sink::Synchronizer {
public:
    TestSynchronizer(const Sink::ResourceContext &context): Sink::Synchronizer(context)
    {

    }

    QMap<QByteArray, std::function<void()>> mSyncCallbacks;

    KAsync::Job<void> synchronizeWithSource(const Sink::QueryBase &query) override
    {
        return KAsync::start([this, query] {
            Q_ASSERT(mSyncCallbacks.contains(query.id()));
            mSyncCallbacks.value(query.id())();
        });
    }

    void createOrModify(const QByteArray &rid, Sink::ApplicationDomain::ApplicationDomainType &entity)
    {
        Sink::Synchronizer::createOrModify("calendar", rid, entity);
    }

    void scanForRemovals(const QSet<QByteArray> &set)
    {
        Sink::Synchronizer::scanForRemovals("calendar", [&](const QByteArray &remoteId) {
            return set.contains(remoteId);
        });
    }

    QByteArray resolveRemoteId(const QByteArray &remoteId) {
        return syncStore().resolveRemoteId("calendar", remoteId);
    }

    void synchronize(std::function<void()> callback, const QByteArray &id = {}, Synchronizer::SyncRequest::RequestOptions options = Synchronizer::SyncRequest::NoOptions) {
        mSyncCallbacks.insert(id, callback);
        Sink::Query query;
        query.setId(id);
        addToQueue(Synchronizer::SyncRequest{query, id, options});
        VERIFYEXEC(processSyncQueue());
    }
};

class SynchronizerTest : public QObject
{
    Q_OBJECT

    QByteArray instanceIdentifier()
    {
        return "synchronizertest.instance1";
    }

    Sink::ResourceContext getContext()
    {
        return Sink::ResourceContext{instanceIdentifier(), "test", Sink::AdaptorFactoryRegistry::instance().getFactories("test")};
    }

    bool queryFor(const QByteArray &sinkId, const QByteArray &type, Sink::Storage::EntityStore &store) {
        bool foundInQuery = false;
        DataStoreQuery dataStoreQuery{{sinkId}, type, store};
        auto resultSet = dataStoreQuery.execute();
        resultSet.replaySet(0, 1, [&](const ResultSet::Result &r) {
            if (r.entity.identifier() == sinkId) {
                foundInQuery = true;
            }
        });
        return foundInQuery;
    }

private slots:
    void initTestCase()
    {
        Sink::Test::initTest();
        Sink::Storage::DataStore{Sink::Store::storageLocation(), instanceIdentifier(), Sink::Storage::DataStore::ReadWrite}.removeFromDisk();
        Sink::AdaptorFactoryRegistry::instance().registerFactory<Sink::ApplicationDomain::Calendar, DomainTypeAdaptorFactory<Sink::ApplicationDomain::Calendar>>("test");
    }

    void init()
    {
        Sink::GenericResource::removeFromDisk(instanceIdentifier());
    }

    /*
     * Ensure we can remove an recreate an entity.
     */
    void testTemporaryRemoval()
    {
        const auto context = getContext();
        Sink::Pipeline pipeline(context, instanceIdentifier());
        Sink::CommandProcessor processor(&pipeline, instanceIdentifier(), Sink::Log::Context{"processor"});

        auto synchronizer = QSharedPointer<TestSynchronizer>::create(context);
        processor.setSynchronizer(synchronizer);

        synchronizer->setSecret("secret");

        synchronizer->synchronize([&] {
            Sink::ApplicationDomain::Calendar calendar;
            calendar.setName("Name");
            synchronizer->createOrModify("1", calendar);
        });

        VERIFYEXEC(processor.processAllMessages());

        const auto sinkId = synchronizer->resolveRemoteId("1");
        QVERIFY(!sinkId.isEmpty());

        {
            Sink::Storage::EntityStore store(context, {"entitystore"});
            QVERIFY(store.contains("calendar", sinkId));
            QVERIFY(store.exists("calendar", sinkId));
            QVERIFY(queryFor(sinkId, "calendar", store));
        }

        //Remove the calendar
        synchronizer->synchronize([&] {
            synchronizer->scanForRemovals({});
        });
        //Process the removal
        VERIFYEXEC(processor.processAllMessages());

        //Ensure we replay the revision generated by the removal.
        //This is necessary to remove the rid mapping
        synchronizer->replayNextRevision().exec();
        {
            Sink::Storage::EntityStore store(context, {"entitystore"});
            QVERIFY(!store.exists("calendar", sinkId));
            QVERIFY(store.contains("calendar", sinkId));
            QVERIFY(!queryFor(sinkId, "calendar", store));
        }

        //Recreate the same calendar
        synchronizer->synchronize([&] {
            Sink::ApplicationDomain::Calendar calendar;
            calendar.setName("Name");
            synchronizer->createOrModify("1", calendar);
        });
        VERIFYEXEC(processor.processAllMessages());

        //Ensure we got a new sink id (if not we failed to remove the rid mapping from the previous instance).
        const auto newSinkId = synchronizer->resolveRemoteId("1");
        QVERIFY(!newSinkId.isEmpty());
        QVERIFY(newSinkId != sinkId);

        {
            Sink::Storage::EntityStore store(context, {"entitystore"});
            QVERIFY(store.contains("calendar", newSinkId));
            QVERIFY(store.exists("calendar", newSinkId));

            // store.readRevisions("calendar", newSinkId, 0, [] (const QByteArray &uid, qint64 revision, const Sink::EntityBuffer &buffer) {
            //     qWarning() << uid << revision << buffer.operation();
            // });

            QVERIFY(!queryFor(sinkId, "calendar", store));
            QVERIFY(queryFor(newSinkId, "calendar", store));
        }
    }

    /*
     * Ensure the flushed content is available during the next sync request
     */
    void testFlush()
    {
        const auto context = getContext();
        Sink::Pipeline pipeline(context, instanceIdentifier());
        Sink::CommandProcessor processor(&pipeline, instanceIdentifier(), Sink::Log::Context{"processor"});

        auto synchronizer = QSharedPointer<TestSynchronizer>::create(context);
        processor.setSynchronizer(synchronizer);

        synchronizer->setSecret("secret");

        QByteArray sinkId;
        synchronizer->synchronize([&] {
            Sink::ApplicationDomain::Calendar calendar;
            calendar.setName("Name");
            synchronizer->createOrModify("1", calendar);
            sinkId = synchronizer->resolveRemoteId("1");
        }, "1");
        QVERIFY(!sinkId.isEmpty());

        //With a flush the calendar should be available during the next sync
        synchronizer->synchronize([&] {
            Sink::Storage::EntityStore store(context, {"entitystore"});
            QVERIFY(store.contains("calendar", sinkId));

        }, "2", Sink::Synchronizer::SyncRequest::RequestFlush);

        VERIFYEXEC(processor.processAllMessages());
    }

};

QTEST_MAIN(SynchronizerTest)
#include "synchronizertest.moc"
