/*
 * Copyright (C) 2008 Funambol, Inc.
 * Copyright (C) 2008-2009 Patrick Ohly <patrick.ohly@gmx.de>
 * Copyright (C) 2009 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

/** @cond API */
/** @addtogroup ClientTest */
/** @{ */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#ifdef ENABLE_INTEGRATION_TESTS

#include "ClientTest.h"
#include "test.h"
#include "ClientTestAssert.h"
#include <SyncSource.h>
#include <TransportAgent.h>
#include <Logging.h>
#include <syncevo/util.h>
#include <syncevo/SyncContext.h>
#include <VolatileConfigNode.h>
#include <syncevo/Cmdline.h>

#include <synthesis/dataconversion.h>

#include <memory>
#include <vector>
#include <set>
#include <utility>
#include <sstream>
#include <iomanip>
#include <fstream>
#include <iostream>
#include <algorithm>
#include <stdarg.h>

#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <boost/tokenizer.hpp>

#include <regex>

#include <syncevo/declarations.h>

namespace CppUnit {

/**
 * behaves like an int and can be compared against one in ASSERT_EQUAL,
 * but includes the item list when being printed
 */
struct ItemCount
{
    SyncEvo::SyncSourceChanges::Items_t m_items;

    ItemCount() {}
    ItemCount(const SyncEvo::SyncSourceChanges::Items_t &items) : m_items(items) {}
    int size() const { return m_items.size(); }
    operator int () const { return size(); }
};

static std::ostream &operator << (ostream &out, const ItemCount &count)
{
    out << count.size() << " ( ";
    for (const std::string &id: count.m_items) {
        out << id << " ";
    }
    out << ")";
    return out;
}

template<> struct assertion_traits<ItemCount>
{
    template <class E> static bool equal(const E &expected, const ItemCount &count) { return expected == count; }
    static std::string toString(const ItemCount &count)
    {
        std::ostringstream out;
        out << count;
        return out.str();
    }
};

/** comparison between arbitrary type A and B */
template <class A, class B>
void assertEquals(const A& expected,
                  const B& actual,
                  SourceLine sourceLine,
                  const std::string &message)
{
    if (!assertion_traits<B>::equal(expected,actual)) {
        Asserter::failNotEqual(assertion_traits<A>::toString(expected),
                               assertion_traits<B>::toString(actual),
                               sourceLine,
                               message);
    }
}

}

SE_BEGIN_CXX

/**
 * A command line using keyring as configured, but no interactive
 * password lookup. The base Cmdline class uses a generic SyncContext
 * which uses a SimpleUserInterface without keyring support.
 */
class TestCmdline : public Cmdline
{
public:
    // If we could inherit the Cmdline constructor, life would be a lot
    // easier... because we can't, we have to copy-and-paste the code
    // and rely on protected inheritance of Cmdline members.
    TestCmdline(const char *arg, ...) :
        Cmdline(std::vector<std::string>())
    {
        va_list argList;
        va_start(argList, arg);
        for (const char *curr = arg;
             curr;
             curr = va_arg(argList, const char *)) {
            m_args.push_back(curr);
        }
        va_end(argList);
        m_argc = m_args.size();
        m_argvArray.reserve(m_args.size());
        for (auto &arg: m_args) {
            m_argvArray.push_back(arg.c_str());
        }
        m_argv = m_argvArray.data();
    }

    virtual SyncContext *createSyncClient() {
        std::unique_ptr<SyncContext> context(new SyncContext(m_server, true));
        auto ui = std::make_shared<SimpleUserInterface>(context->getKeyring());
        context->setUserInterface(ui);
        return context.release();
    }
};

static set<ClientTest::Cleanup_t> cleanupSet;

/**
 * true when running as server,
 * relevant for sources instantiated by us
 * and testConversion, which does not work in
 * server mode (Synthesis engine not in the right
 * state when we try to run the test)
 */
static bool isServerMode()
{
    const char *serverMode = getenv("CLIENT_TEST_MODE");
    return serverMode && !strcmp(serverMode, "server");
}

/**
 * CLIENT_TEST_SERVER env variable or "" if unset
 */
std::string currentServer()
{
    const char *tmp = getenv("CLIENT_TEST_SERVER");
    return tmp ? tmp : "";
}

/**
 * This function checks whether the data on the server is accessible
 * directly. This is the case for tests where the server side is
 * syncevo-http-server or the sync is local. In both cases, this
 * method checks the config of <server>_1 (the first client's sync config)
 * to find the peer and the uri of the given local source. The peer
 * is expected to a source under that name (no alias!).
 *
 * CLIENT_TEST_SERVER must be set. <server>_1 must exist and (if
 * syncing via HTTP) have a deviceId that matches a remoteDeviceId in
 * the config used by syncevo-http-server.
 *
 * @return pair of <peer sync config> + <peer source name>; sync config name empty if not found
 */
std::pair<std::string, std::string> getPeerConfig(const std::string &source)
{
    static const char LOCAL_SYNC[] = "local://";
    SyncConfig local(currentServer() + "_1");
    std::vector<std::string> syncURLs = local.getSyncURL();
    std::shared_ptr<PersistentSyncSourceConfig> sourceConfig(local.getSyncSourceConfig(source));
    std::string uri = sourceConfig->getURI();
    if (uri.empty()) {
        uri = source;
    }
    std::string peerConfig;
    if (syncURLs.size() == 1) {
        const std::string &syncURL = syncURLs.front();
        if (boost::starts_with(syncURL, LOCAL_SYNC)) {
            // Local sync. "target-config" is implied and may be relevant
            // later when using the peer source.
            peerConfig = syncURL.substr(strlen(LOCAL_SYNC));
            if (boost::starts_with(peerConfig, "@")) {
                peerConfig = "target-config" + peerConfig;
            }
        }
    }

    if (peerConfig.empty()) {
        // Check for local HTTP server.
        std::string deviceId = local.getDevID();
        for (const auto &peer: SyncConfig::getConfigs()) {
            SyncConfig remote(peer.first);
            if (remote.getRemoteDevID() == deviceId) {
                peerConfig = peer.first;
                break;
            }
        }
    }

    return std::make_pair(peerConfig, uri);
}

/**
 * Tests involving a specific peer use testcases/synctests/<server
 * name>/<source name>/<test name>/<aspect>, where <aspect> is test
 * specific. The resulting string typically references a directory
 * with individual items. <aspect> can be empty.
 */
std::string getPeerTestdata(const std::string &source, const std::string &test, const std::string &aspect)
{
    std::string path = StringPrintf("testcases/synctests/%s/%s/%s/%s",
                                    currentServer().c_str(),
                                    source.c_str(),
                                    test.c_str(),
                                    aspect.c_str());
    return path;
}

/**
 * CLIENT_TEST_NUM_ITEMS env variable or 100
 */
int defNumItems()
{
    char *numitems = getenv("CLIENT_TEST_NUM_ITEMS");
    return numitems ? atoi(numitems) : 100;
}

static SyncMode RefreshFromPeerMode()
{
    return isServerMode() ? SYNC_REFRESH_FROM_CLIENT : SYNC_REFRESH_FROM_SERVER;
}

static SyncMode RefreshFromLocalMode()
{
    return isServerMode() ? SYNC_REFRESH_FROM_SERVER : SYNC_REFRESH_FROM_CLIENT;
}

static SyncMode OneWayFromPeerMode()
{
    return isServerMode() ? SYNC_ONE_WAY_FROM_CLIENT : SYNC_ONE_WAY_FROM_SERVER;
}

static SyncMode OneWayFromLocalMode()
{
    return isServerMode() ? SYNC_ONE_WAY_FROM_SERVER : SYNC_ONE_WAY_FROM_CLIENT;
}

/**
 * remove a certain property from buffer, return removed line
 */
static string stripProperty(std::string &data, const std::string &prop)
{
    std::string res;

    size_t start = data.find(prop);
    if (start != data.npos) {
        size_t end = data.find('\n', start);
        if (end != data.npos) {
            size_t len = end + 1 - start;
            res = data.substr(start, len);
            data.erase(start, len);
        }
    }

    return res;
}

/**
 * insert a property (must include line end) before the end of an item
 */
static void insertProperty(std::string &data,
                           const std::string &prop,
                           const std::string &endProp = "END:VEVENT")
{
    size_t pos = data.find(endProp);
    data.insert(pos, prop);
}

/**
 * remove parameter in all properties
 */
static void stripParameters(std::string &data,
                            const std::string &param)
{
    while (true) {
        size_t start = data.find(";" + param + "=");
        if (start == data.npos) {
            break;
        }
        size_t end = data.find_first_of(";:", start + 1);
        if (end == data.npos) {
            break;
        }
        data.erase(start, end - start);
    }
}

static void stripComponent(std::string &data,
                           const std::string &comp)
{
    size_t start = data.find("BEGIN:" + comp);
    if (start != data.npos) {
        size_t end = data.find("END:" + comp);
        if (end != data.npos) {
            end = data.find('\n', end);
            if (end != data.npos) {
                data.erase(start, end + 1 - start);
            }
        }
    }
}

/**
 * Using this pointer automates the open()/beginSync()/endSync()/close()
 * life cycle: it automatically calls these functions when a new
 * pointer is assigned or deleted.
 *
 * Anchors are stored globally in a hash which uses the tracking node
 * name as key. This name happens to be the unique file path that
 * is created for each source (see TestEvolution::createSource() and
 * SyncConfig::getSyncSourceNodes()).
 */
class TestingSyncSourcePtr : public std::unique_ptr<TestingSyncSource>
{
    typedef std::unique_ptr<TestingSyncSource> base_t;
    bool m_active;

    static StringMap m_anchors;
    static std::string m_testName;

public:
    TestingSyncSourcePtr() : m_active(false) {}
    TestingSyncSourcePtr(const TestingSyncSourcePtr &other) : m_active(false) {
        CPPUNIT_ASSERT(!other.get());
    }
    ~TestingSyncSourcePtr()
    {
        // We can skip the full cleanup if the test has already failed.
        // Also avoids letting an exception escape from the
        // destructor during exception handling (= program aborted!)
        // when the endSync() call invoked by reset() needs to
        // report a proble. CT_ASSERT_NO_THROW() itself catches that
        // exception, but then forwards it, and thus does not
        // prevent the exception from escaping.
        if (!std::uncaught_exception()) {
            CT_ASSERT_NO_THROW(reset(nullptr));
        }
    }

    // Not implemented, only declared to keep cppcheck happy.
    TestingSyncSourcePtr &operator = (const TestingSyncSourcePtr &other);

    enum Flags {
        SLOW,           /**< erase anchor, start accessing database from scratch */
        INCREMENTAL     /**< allow source to do incremental data read */
    };

    void reset(std::unique_ptr<TestingSyncSource> source = {}, Flags flags = INCREMENTAL)
    {
        if (get() && m_active) {
            stopAccess();
        }
        base_t::reset(source.release());
        if (get()) {
            startAccess(flags);
        }
    }

    /**
     * done automatically as part of reset(), only to be called
     * after an explicit stopAccess()
     */
    void startAccess(Flags flags = INCREMENTAL)
    {
        CT_ASSERT(get());
        CT_ASSERT(!m_active);
        int delay = atoi(getEnv("CLIENT_TEST_SOURCE_DELAY", "0"));
        if (delay) {
            CLIENT_TEST_LOG("CLIENT_TEST_SOURCE_DELAY: sleep for %d seconds", delay);
            sleep(delay);
        }
        CT_ASSERT_NO_THROW(get()->open());
        string node = get()->getTrackingNode()->getName();
        string anchor = m_anchors[node];
        if (flags == SLOW) {
            anchor = "";
        }
        get()->beginSync(anchor, "");
        if (isServerMode()) {
            CT_ASSERT_NO_THROW(get()->enableServerMode());
        }
        // the replaced m_endSession callback was invoked here,
        // which shouldn't have been necessary - not calling
        // m_endDataWrite post-signal at the moment
        // CT_ASSERT_NO_THROW(get()->getOperations().m_endDataWrite.getPostSignal()());
        m_active = true;
    }

    /**
     * finish change tracking, source must be activated again
     * with startAccess()
     */
    void stopAccess()
    {
        CT_ASSERT(get());
        CT_ASSERT(m_active);
        m_active = false;
        char *dummy = const_cast<char *>("testing-source");
        CT_ASSERT_NO_THROW(get()->getOperations().m_endDataWrite.getPostSignal()(*get(), OPERATION_FINISHED, sysync::LOCERR_OK, true, &dummy));
        string node = get()->getTrackingNode()->getName();
        string anchor;
        CT_ASSERT_NO_THROW(anchor = get()->endSync(true));
        m_anchors[node] = anchor;
        CT_ASSERT_NO_THROW(get()->close());
    }
};

StringMap TestingSyncSourcePtr::m_anchors;
std::string TestingSyncSourcePtr::m_testName;

bool SyncOptions::defaultWBXML()
{
    const char *t = getenv("CLIENT_TEST_XML");
    if (t && (!strcmp(t, "1") || !strcasecmp(t, "t"))) {
        // use XML
        return false;
    } else {
        return true;
    }
}

std::list<std::string> listItemsOfType(TestingSyncSource *source, int state)
{
    std::list<std::string> res;

    for (const string &luid: source->getItems(SyncSourceChanges::State(state))) {
        res.push_back(luid);
    }
    return res;
}
static std::list<std::string> listNewItems(TestingSyncSource *source) { return listItemsOfType(source, SyncSourceChanges::NEW); }
static std::list<std::string> listUpdatedItems(TestingSyncSource *source) { return listItemsOfType(source, SyncSourceChanges::UPDATED); }
static std::list<std::string> listDeletedItems(TestingSyncSource *source) { return listItemsOfType(source, SyncSourceChanges::DELETED); }
static std::list<std::string> listItems(TestingSyncSource *source) { return listItemsOfType(source, SyncSourceChanges::ANY); }

static CppUnit::ItemCount countItemsOfType(TestingSyncSource *source, int type) { return source->getItems(SyncSourceChanges::State(type)); }
static CppUnit::ItemCount countNewItems(TestingSyncSource *source) { return countItemsOfType(source, SyncSourceChanges::NEW); }
static CppUnit::ItemCount countUpdatedItems(TestingSyncSource *source) { return countItemsOfType(source, SyncSourceChanges::UPDATED); }
static CppUnit::ItemCount countDeletedItems(TestingSyncSource *source) { return countItemsOfType(source, SyncSourceChanges::DELETED); }
static CppUnit::ItemCount countItems(TestingSyncSource *source) { return countItemsOfType(source, SyncSourceChanges::ANY); }

/** insert new item, return LUID */
static std::string importItem(TestingSyncSource *source, const ClientTestConfig &config, std::string &data)
{
    CT_ASSERT(source);
    if (data.size()) {
        SyncSourceRaw::InsertItemResult res;
        SOURCE_ASSERT_NO_FAILURE(source, res = source->insertItemRaw("", config.m_mangleItem(data, false, "")));
        CT_ASSERT(!res.m_luid.empty());
        return res.m_luid;
    } else {
        return "";
    }
}

/** overwrite existing item */
static void updateItem(TestingSyncSource *source, std::string &data, const std::string &luid)
{
    CT_ASSERT(source);
    CT_ASSERT(!data.empty());
    CT_ASSERT(!luid.empty());

    SyncSourceRaw::InsertItemResult res;
    SOURCE_ASSERT_NO_FAILURE(source, res = source->insertItemRaw(luid, data));
    CT_ASSERT_EQUAL(luid, res.m_luid);
}

/** remove existing item */
static void removeItem(TestingSyncSource *source, const std::string &luid)
{
    CT_ASSERT(source);
    CT_ASSERT(!luid.empty());

    SOURCE_ASSERT_NO_FAILURE(source, source->deleteItem(luid));
}

static void restoreStorage(const ClientTest::Config &config, ClientTest &client)
{
}

static void backupStorage(const ClientTest::Config &config, ClientTest &client)
{
}

/** adds the supported tests to the instance itself */
void LocalTests::addTests() {
    if (config.m_createSourceA) {
        ADD_TEST(LocalTests, testOpen);
        ADD_TEST(LocalTests, testIterateTwice);
        ADD_TEST(LocalTests, testDelete404);
        ADD_TEST(LocalTests, testReadItem404);
        if (!config.m_insertItem.empty()) {
            ADD_TEST(LocalTests, testSimpleInsert);
            ADD_TEST(LocalTests, testLocalDeleteAll);
            ADD_TEST(LocalTests, testComplexInsert);
            if (config.m_uniqueID) {
                ADD_TEST(LocalTests, testInsertTwice);
            }

            if (!config.m_updateItem.empty()) {
                ADD_TEST(LocalTests, testLocalUpdate);

                if (config.m_createSourceB) {
                    ADD_TEST(LocalTests, testChanges);
                    ADD_TEST(LocalTests, testChangesMultiCycles);

                    if (!config.m_linkedSources.empty()) {
                        ADD_TEST(LocalTests, testLinkedSources);
                    }
                }
            }

            if (config.m_import &&
                config.m_dump &&
                config.m_compare &&
                !config.m_testcases.empty()) {
                ADD_TEST(LocalTests, testImport);
                ADD_TEST(LocalTests, testImportDelete);
                if (!config.m_essentialProperties.empty()) {
                    ADD_TEST(LocalTests, testRemoveProperties);
                }
            }

            if (!config.m_templateItem.empty()) {
                ADD_TEST(LocalTests, testManyChanges);
            }

            // create a sub-suite for each set of linked items
            for (int i = 0; i < (int)config.m_linkedItems.size(); i++) {
                const ClientTestConfig::LinkedItems_t &items = config.m_linkedItems[i];
                CppUnit::TestSuite *linked = new CppUnit::TestSuite(getName() + "::LinkedItems" + items.m_name);
                ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsParent);
                if (config.m_linkedItemsRelaxedSemantic) {
                    ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsChild);
                }
                ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsParentChild);
                if (items[1].find("RECURRENCE-ID") != items[1].npos &&
                    config.m_sourceKnowsItemSemantic) {
                    ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsInsertBothUpdateChildNoIDs);
                }
                if (config.m_linkedItemsRelaxedSemantic) {
                    ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsChildParent);
                }
                if (config.m_linkedItemsRelaxedSemantic) {
                    ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsChildChangesParent);
                }
                if (config.m_linkedItemsRelaxedSemantic) {
                    ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsRemoveParentFirst);
                }
                ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsRemoveNormal);
                if (config.m_sourceKnowsItemSemantic) {
                    ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsInsertParentTwice);
                    if (config.m_linkedItemsRelaxedSemantic) {
                        ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsInsertChildTwice);
                    }
                }
                ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsParentUpdate);
                if (config.m_linkedItemsRelaxedSemantic) {
                    ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsUpdateChild);
                    if (items[1].find("RECURRENCE-ID") != items[1].npos &&
                        config.m_sourceKnowsItemSemantic) {
                        ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsUpdateChildNoIDs);
                    }
                }
                ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsInsertBothUpdateChild);
                ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsInsertBothUpdateParent);

                // tests independent of data, only add to default item set
                if (i == 0) {
                    ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsSingle404);
                    ADD_TEST_TO_SUITE(linked, LocalTests, testLinkedItemsMany404);
                }
                addTest(linked);
            }

            // Create a sub-suite for each set of linked items.
            // items.size() can be fairly large for these tests,
            // so avoid testing all possible combinations.
            for (const ClientTestConfig::LinkedItems_t &items:
                          config.m_linkedItemsSubset) {
                CppUnit::TestSuite *linked = new CppUnit::TestSuite(getName() + "::LinkedItems" + items.m_name);
                int stride = (items.size() + 4) / 5;
                for (int start = 0;
                     (size_t)start < items.size();
                     start += stride ) {
                    for (int skip = 0;
                         !skip || (size_t)(start + skip + 1) < items.size();
                         skip++) {
                        ADD_TEST_TO_SUITE_SUFFIX(linked, LocalTests, testSubset,
                                                 StringPrintf("Start%dSkip%d", start, skip));
                    }
                    // add a test which uses start, start + 1 and last item
                    // if that leads to a gap (EXDATE)
                    if (start > 0 && items.size() - start > 3) {
                        ADD_TEST_TO_SUITE_SUFFIX(linked, LocalTests, testSubset,
                                                 StringPrintf("Start%dExdate", start));
                    }
                }
                addTest(linked);
            }
        }
    }
}

std::string LocalTests::insert(const CreateSource &createSource, const std::string &data, bool relaxed, std::string *inserted, const std::string &uniqueUIDSuffix) {
    restoreStorage(config, client);

    // create source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));

    // count number of already existing items
    int numItems = 0;
    CT_ASSERT_NO_THROW(numItems = countItems(source.get()));
    SyncSourceRaw::InsertItemResult res;
    std::string mangled = config.m_mangleItem(data, false, uniqueUIDSuffix);
    if (inserted) {
        *inserted = mangled;
    }
    SOURCE_ASSERT_NO_FAILURE(source.get(), res = source->insertItemRaw("", mangled));
    CT_ASSERT(!res.m_luid.empty());

    bool updated = false;
    if (res.m_state == ITEM_NEEDS_MERGE) {
        // conflict detected, overwrite existing item as done in the past
        std::string luid = res.m_luid;
        SOURCE_ASSERT_NO_FAILURE(source.get(), res = source->insertItemRaw(luid, mangled));
        CT_ASSERT_EQUAL(luid, res.m_luid);
        CT_ASSERT(res.m_state == ITEM_OKAY);
        updated = true;
    }

    // delete source again
    CT_ASSERT_NO_THROW(source.reset());

    if (!relaxed) {
        // two possible results:
        // - a new item was added
        // - the item was matched against an existing one
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));
        CT_ASSERT_EQUAL(numItems + ((res.m_state == ITEM_REPLACED || res.m_state == ITEM_MERGED || updated) ? 0 : 1),
                        countItems(source.get()));
        CT_ASSERT_EQUAL(0, countNewItems(source.get()));
        CT_ASSERT_EQUAL(0, countUpdatedItems(source.get()));
        CT_ASSERT_EQUAL(0, countDeletedItems(source.get()));
    }
    backupStorage(config, client);

    return res.m_luid;
}

/** deletes specific item locally via sync source */
static std::string updateItem(const CreateSource &createSource, const ClientTestConfig &config, const std::string &uid, const std::string &data, std::string *updated = nullptr) {
    std::string newuid;

    CT_ASSERT(createSource.createSource);

    // create source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));

    // insert item
    SyncSourceRaw::InsertItemResult res;
    std::string mangled;
    CT_ASSERT_NO_THROW(mangled = config.m_mangleItem(data, true, ""));
    if (updated) {
        *updated = mangled;
    }
    SOURCE_ASSERT_NO_FAILURE(source.get(), res = source->insertItemRaw(uid, mangled.c_str()));
    SOURCE_ASSERT(source.get(), !res.m_luid.empty());

    return res.m_luid;
}

/** updates specific item locally via sync source */
static void removeItem(const CreateSource &createSource, const std::string &luid)
{
    CT_ASSERT(createSource.createSource);

    // create source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));

    // remove item
    SOURCE_ASSERT_NO_FAILURE(source.get(), source->deleteItem(luid));
}

void LocalTests::update(const CreateSource &createSource, const std::string &data, bool check, const std::string &uniqueUIDSuffix) {
    CT_ASSERT(createSource.createSource);

    restoreStorage(config, client);

    // create source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));

    // get existing item, then update it
    SyncSourceChanges::Items_t::const_iterator it;
    SOURCE_ASSERT_NO_FAILURE(source.get(), it = source->getAllItems().begin());
    CT_ASSERT(it != source->getAllItems().end());
    string luid = *it;
    SyncSourceRaw::InsertItemResult res;
    std::string mangled = config.m_mangleItem(data, true, uniqueUIDSuffix);
    SOURCE_ASSERT_NO_FAILURE(source.get(), res = source->insertItemRaw(luid, mangled));
    CT_ASSERT_NO_THROW(source.reset());
    CT_ASSERT_EQUAL(luid, res.m_luid);
    CT_ASSERT_EQUAL(ITEM_OKAY, res.m_state);

    if (!check) {
        return;
    }

    // check that the right changes are reported when reopening the source
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));
    CT_ASSERT_EQUAL(1, countItems(source.get()));
    CT_ASSERT_EQUAL(0, countNewItems(source.get()));
    CT_ASSERT_EQUAL(0, countUpdatedItems(source.get()));
    CT_ASSERT_EQUAL(0, countDeletedItems(source.get()));
    
    SOURCE_ASSERT_NO_FAILURE(source.get(), it = source->getAllItems().begin());
    CT_ASSERT(it != source->getAllItems().end());
    CT_ASSERT_EQUAL(luid, *it);

    backupStorage(config, client);
}

void LocalTests::update(const CreateSource &createSource, const std::string &data, const std::string &luid) {
    CT_ASSERT(createSource.createSource);

    restoreStorage(config, client);
    // create source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));

    // update it
    SOURCE_ASSERT_NO_FAILURE(source.get(), source->insertItemRaw(luid, config.m_mangleItem(data, true, "")));

    backupStorage(config, client);
}

/** deletes all items locally via sync source */
void LocalTests::deleteAll(const CreateSource &createSource) {
    CT_ASSERT(createSource.createSource);

    restoreStorage(config, client);
    // create source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));

    // delete all items
    SOURCE_ASSERT_NO_FAILURE(source.get(), source->removeAllItems());
    CT_ASSERT_NO_THROW(source.reset());

    // check that all items are gone
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));
    SOURCE_ASSERT_MESSAGE(
        "should be empty now",
        source.get(),
        countItems(source.get()) == 0);
    CT_ASSERT_EQUAL( 0, countNewItems(source.get()) );
    CT_ASSERT_EQUAL( 0, countUpdatedItems(source.get()) );
    CT_ASSERT_EQUAL( 0, countDeletedItems(source.get()) );
    backupStorage(config, client);
}

/** deletes specific item locally via sync source */
static void deleteItem(const CreateSource &createSource, const std::string &uid) {
    CT_ASSERT(createSource.createSource);

    // create source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));

    // delete item
    SOURCE_ASSERT_NO_FAILURE(source.get(), source->deleteItem(uid));
}

/**
 * takes two databases, exports them,
 * then compares them using synccompare
 *
 * @param refFile      existing file with source reference items, nullptr uses a dump of sync source A instead
 * @param copy         a sync source which contains the copied items, begin/endSync will be called
 * @param raiseAssert  raise assertion if comparison yields differences (defaults to true)
 */
bool LocalTests::compareDatabases(const char *refFile, TestingSyncSource &copy, bool raiseAssert) {
    CT_ASSERT(config.m_dump);

    std::string sourceFile, copyFile;

    if (refFile) {
        sourceFile = refFile;
    } else {
        sourceFile = getCurrentTest() + ".A.test.dat";
        simplifyFilename(sourceFile);
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
        SOURCE_ASSERT_EQUAL(source.get(), 0, config.m_dump(client, *source.get(), sourceFile));
        CT_ASSERT_NO_THROW(source.reset());
    }

    copyFile = getCurrentTest() + ".B.test.dat";
    simplifyFilename(copyFile);
    CT_ASSERT_EQUAL(0, config.m_dump(client, copy, copyFile));

    return compareDatabases(sourceFile, copyFile);
}

bool LocalTests::compareDatabases(const std::string &refFile, const std::string &actualFile, bool raiseAssert)
{
    bool equal = false;
    CT_ASSERT_NO_THROW(equal = config.m_compare(client, refFile.c_str(), actualFile.c_str()));
    CT_ASSERT(!raiseAssert || equal);

    return equal;
}

/**
 * compare data in source with vararg list of std::string pointers, nullptr terminated
 */
void LocalTests::compareDatabases(TestingSyncSource *copy,
                                  ...)
{
    std::string sourceFile = getCurrentTest() + ".ref.test.dat";
    simplifyFilename(sourceFile);
    ofstream out(sourceFile.c_str());
    va_list ap;
    va_start(ap, copy);
    std::string *item;
    while ((item = va_arg(ap, std::string *)) != nullptr) {
        out << *item;
    }
    va_end(ap);
    out.close();
    compareDatabases(sourceFile.c_str(), *copy);
}

void LocalTests::compareDatabasesRef(TestingSyncSource &copy,
                                     const std::list<std::string> &items)
{
    std::string sourceFile = getCurrentTest() + ".ref.test.dat";
    simplifyFilename(sourceFile);
    ofstream out(sourceFile.c_str());
    for (const std::string &item: items) {
        out << item;
    }
    out.close();
    compareDatabases(sourceFile.c_str(), copy);
}


std::string LocalTests::createItem(int item, const std::string &revision, int size)
{
    std::string data = config.m_mangleItem(config.m_templateItem, false, "");
    std::stringstream prefix;

    // string to be inserted at start of unique properties;
    // avoid adding white space (not sure whether it is valid for UID)
    prefix << std::setfill('0') << std::setw(3) << item << "-";

    for (std::string curProp:
                   boost::tokenizer< boost::char_separator<char> >(config.m_uniqueProperties,
                                                                   boost::char_separator<char>(":"))) {
        std::string property;
        // property is expected to not start directly at the
        // beginning
        property = "\n";
        property += curProp;
        property += ":";
        size_t off = data.find(property);
        if (off != data.npos) {
            data.insert(off + property.size(), prefix.str());
        }
    }
    boost::replace_all(data, "<<UNIQUE>>", prefix.str());
    boost::replace_all(data, "<<REVISION>>", revision);
    if (size > 0 && (int)data.size() < size) {
        int additionalBytes = size - (int)data.size();
        int added = 0;
        /* vCard 2.1 and vCal 1.0 need quoted-printable line breaks */
        bool quoted = data.find("VERSION:1.0") != data.npos ||
            data.find("VERSION:2.1") != data.npos;
        size_t toreplace = 1;

        CT_ASSERT(!config.m_sizeProperty.empty());

        /* stuff the item so that it reaches at least that size */
        size_t off = data.find(config.m_sizeProperty);
        CT_ASSERT(off != data.npos);
        std::stringstream stuffing;
        if (quoted) {
            stuffing << ";ENCODING=QUOTED-PRINTABLE:";
        } else {
            stuffing << ":";
        }

        // insert after the first line, it often acts as the summary
        if (data.find("BEGIN:VJOURNAL") != data.npos) {
            size_t start = data.find(":", off);
            CT_ASSERT( start != data.npos );
            size_t eol = data.find("\\n", off);
            CT_ASSERT( eol != data.npos );
            stuffing << data.substr(start + 1, eol - start + 1);
            toreplace += eol - start + 1;
        }

        while(added < additionalBytes) {
            int linelen = 0;

            while(added + 4 < additionalBytes &&
                  linelen < 60) {
                stuffing << 'x';
                added++;
                linelen++;
            }
            // insert line breaks to allow folding
            if (quoted) {
                stuffing << "x=0D=0Ax";
                added += 8;
            } else {
                stuffing << "x\\nx";
                added += 4;
            }
        }
        off = data.find(":", off);
        data.replace(off, toreplace, stuffing.str());
    }

    return data;
}


/**
 * insert artificial items, number of them 100
 * unless passed explicitly
 *
 * @param createSource    a factory for the sync source that is to be used
 * @param startIndex      IDs are generated starting with this value
 * @param numItems        number of items to be inserted if non-null, otherwise config.numItems is used
 * @param size            minimum size for new items
 * @return LUIDs of all inserted items
 */
std::list<std::string> LocalTests::insertManyItems(const CreateSource &createSource, int startIndex, int numItems, int size) {
    std::list<std::string> luids;

    CT_ASSERT(!config.m_templateItem.empty());

    restoreStorage(config, client);
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
    CT_ASSERT(startIndex > 1 || !countItems(source.get()));

    int firstIndex = startIndex;
    if (firstIndex < 0) {
        firstIndex = 1;
    }
    int lastIndex = firstIndex + (numItems >= 1 ? numItems : defNumItems()) - 1;
    for (int item = firstIndex; item <= lastIndex; item++) {
        std::string data = createItem(item, "", size);
        luids.push_back(importItem(source.get(), config, data));
    }
    backupStorage(config, client);

    return luids;
}

std::list<std::string> LocalTests::insertManyItems(TestingSyncSource *source, int startIndex, int numItems, int size) {
    std::list<std::string> luids;

    CT_ASSERT(!config.m_templateItem.empty());

    CT_ASSERT(startIndex > 1 || !countItems(source));
    int firstIndex = startIndex;
    if (firstIndex < 0) {
        firstIndex = 1;
    }
    int lastIndex = firstIndex + (numItems >= 1 ? numItems : defNumItems()) - 1;
    for (int item = firstIndex; item <= lastIndex; item++) {
        std::string data = createItem(item, "", size);
        luids.push_back(importItem(source, config, data));
    }

    return luids;
}

void LocalTests::updateManyItems(const CreateSource &createSource, int startIndex, int numItems, int size,
                                 int revision,
                                 std::list<std::string> &luids,
                                 int offset)
{
    CT_ASSERT(!config.m_templateItem.empty());

    restoreStorage(config, client);
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));

    int firstIndex = startIndex;
    if (firstIndex < 0) {
        firstIndex = 1;
    }
    int lastIndex = firstIndex + (numItems >= 1 ? numItems : defNumItems()) - 1;
    std::string revstring = StringPrintf("REVISION #%d", revision);
    auto it = luids.begin();
    for (int i = 0; i < offset && it != luids.end(); i++, ++it) {}
    for (int item = firstIndex;
         item <= lastIndex && it != luids.end();
         item++, ++it) {
        std::string data = createItem(item, revstring, size);
        updateItem(source.get(), data, *it);
    }
    backupStorage(config, client);
}

void LocalTests::removeManyItems(const CreateSource &createSource, int numItems,
                                 std::list<std::string> &luids,
                                 int offset)
{
    restoreStorage(config, client);
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));

    auto it = luids.begin();
    for (int i = 0; i < offset && it != luids.end(); i++, ++it) {}
    for (int item = 0;
         item < numItems && it != luids.end();
         item++, ++it) {
        removeItem(source.get(), *it);
    }
    backupStorage(config, client);
}

// update every single item in the database
void LocalTests::updateData(const CreateSource &createSource) {
    // check additional requirements
    CT_ASSERT(config.m_update);

    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSource()));
    for (const string &luid: source->getAllItems()) {
        string item;
        CT_ASSERT_NO_THROW(source->readItemRaw(luid, item));
        CT_ASSERT_NO_THROW(config.m_update(item));
        CT_ASSERT_NO_THROW(source->insertItemRaw(luid, item));
    }
    CT_ASSERT_NO_THROW(source.reset());
}


// creating sync source
void LocalTests::testOpen() {
    // check requirements
    CT_ASSERT(config.m_createSourceA);

    // Intentionally use the plain unique_ptr here and
    // call open directly. That way it is a bit more clear
    // what happens and where it fails, if it fails.
    std::unique_ptr<TestingSyncSource> source;
    CT_ASSERT_NO_THROW(source = createSourceA());
    // got a sync source?
    CT_ASSERT(source.get() != 0);
    // can it be opened?
    SOURCE_ASSERT_NO_FAILURE(source.get(), source->open());
    // delete it
    CT_ASSERT_NO_THROW(source.reset());
}

// restart scanning of items
void LocalTests::testIterateTwice() {
    // check requirements
    CT_ASSERT(config.m_createSourceA);

    // open source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
    SOURCE_ASSERT_MESSAGE(
        "iterating twice should produce identical results",
        source.get(),
        countItems(source.get()) == countItems(source.get()));
}

// deleteItem() must raise 404 for unknown item
void LocalTests::testDelete404() {
    // check requirements
    CT_ASSERT(config.m_createSourceA);

    // open source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
    SyncMLStatus status = STATUS_OK;
    try {
        source->deleteItem("no-such-item");
    } catch (const StatusException &ex) {
        status = ex.syncMLStatus();
    }
    CT_ASSERT_EQUAL(STATUS_NOT_FOUND, status);
}

// deleteItem() must raise 404 for unknown item
void LocalTests::testReadItem404() {
    // check requirements
    CT_ASSERT(config.m_createSourceA);

    // open source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
    SyncMLStatus status = STATUS_OK;
    try {
        std::string data;
        source->readItem("no-such-item", data);
    } catch (const StatusException &ex) {
        status = ex.syncMLStatus();
    }
    CT_ASSERT_EQUAL(STATUS_NOT_FOUND, status);
}

void LocalTests::doInsert(bool withUID)
{
    // check requirements
    CT_ASSERT(!config.m_insertItem.empty());
    CT_ASSERT(config.m_createSourceA);

    std::string item = config.m_insertItem;
    if (!withUID) {
        CT_ASSERT_NO_THROW(stripProperty(item, "UID"));
    }
    CT_ASSERT_NO_THROW(insert(createSourceA, item));
}

// insert one contact without clearing the source first
void LocalTests::testSimpleInsert() {
    // Insert the item without the UID. There is no guarantee that the
    // item wasn't already inserted before (database not cleaned by
    // test) and some backends (CalDAV) will catch an attempt to
    // add the same item twice.
    doInsert(false);
}

// delete all items
void LocalTests::testLocalDeleteAll() {
    // check requirements
    CT_ASSERT(!config.m_insertItem.empty());
    CT_ASSERT(config.m_createSourceA);

    // make sure there is something to delete, then delete again
    std::string item = config.m_insertItem;
    CT_ASSERT_NO_THROW(stripProperty(item, "UID"));
    CT_ASSERT_NO_THROW(insert(createSourceA, item));
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
}

// clean database, then insert
void LocalTests::testComplexInsert() {
    CT_ASSERT(config.m_createSourceA);
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    CT_ASSERT_NO_THROW(doInsert());
    CT_ASSERT_NO_THROW(testIterateTwice());
}

// insert the same item (identified by UID) twice => either
// ITEM_NEEDS_MERGE, ITEM_REPLACED or ITEM_MERGED are acceptable
void LocalTests::testInsertTwice() {
    CT_ASSERT(config.m_createSourceA);
    CT_ASSERT(!config.m_insertItem.empty());
    CT_ASSERT(config.m_insertItem.find("\nUID:") != std::string::npos);
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    // create source
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));

    // mangle data once
    std::string data = config.m_mangleItem(config.m_insertItem, false, "");

    // insert new item
    SyncSourceRaw::InsertItemResult first;
    SOURCE_ASSERT_NO_FAILURE(source.get(), first = source->insertItemRaw("", data));
    CT_ASSERT_EQUAL(ITEM_OKAY, first.m_state);

    // and again
    SyncSourceRaw::InsertItemResult second;
    SOURCE_ASSERT_NO_FAILURE(source.get(), second = source->insertItemRaw("", data));
    CLIENT_TEST_LOG("item %s",
                    second.m_state == ITEM_NEEDS_MERGE ? "needs to be merged" :
                    second.m_state == ITEM_REPLACED ? "was replaced" :
                    second.m_state == ITEM_MERGED ? "was merged" :
                    second.m_state == ITEM_OKAY ? "was added, which is broken!" :
                    "unknown result ?!");
    if (config.m_sourceKnowsItemSemantic) {
        CT_ASSERT(second.m_state == ITEM_NEEDS_MERGE || second.m_state == ITEM_REPLACED || second.m_state == ITEM_MERGED);
        CT_ASSERT_EQUAL(first.m_luid, second.m_luid);
    } else {
        CT_ASSERT(second.m_state == ITEM_OKAY);
        CT_ASSERT(first.m_luid != second.m_luid);
    }
    if (second.m_state == ITEM_REPLACED || second.m_state == ITEM_MERGED) {
        CT_ASSERT(first.m_revision != second.m_revision);
    }
}

// clean database, insert item, update it
void LocalTests::testLocalUpdate() {
    // check additional requirements
    CT_ASSERT(!config.m_updateItem.empty());
    CT_ASSERT(config.m_createSourceA);

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    CT_ASSERT_NO_THROW(doInsert());
    CT_ASSERT_NO_THROW(update(createSourceA, config.m_updateItem));
}

// Complex sequence of changes, with one restarted instance of source
// B to observe the changes or multiple instances of it.
// Changes are made both via source A and via source B itself.
void LocalTests::doChanges(bool restart) {
    SyncSourceChanges::Items_t::const_iterator it, it2;

    // check additional requirements
    CT_ASSERT(config.m_createSourceB);
    CT_ASSERT(config.m_createSourceA);

    CLIENT_TEST_LOG("clean via source A");
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    CLIENT_TEST_LOG("insert item via source A");
    CT_ASSERT_NO_THROW(doInsert());

    CLIENT_TEST_LOG("clean changes in sync source B by creating and closing it");
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceB()));
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());

    CLIENT_TEST_LOG("no new changes now in source B");
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
    string item;
    string luid;
    SOURCE_ASSERT_NO_FAILURE(source.get(), it = source->getAllItems().begin());
    CT_ASSERT(it != source->getAllItems().end());
    luid = *it;
    // It is not required for incremental syncing that sources must be
    // able to return unchanged items. For example, ActiveSyncSource doesn't support
    // it because it gets only IDs and data of added or updated items.
    // Don't test it.
    // SOURCE_ASSERT_NO_FAILURE(source.get(), source->readItem(*it, item));
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());

    CLIENT_TEST_LOG("delete item again via sync source A");
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    CLIENT_TEST_LOG("check for deleted item via source B");
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countDeletedItems(source.get()));
    SOURCE_ASSERT_NO_FAILURE(source.get(), it = source->getDeletedItems().begin());
    CT_ASSERT(it != source->getDeletedItems().end());
    CT_ASSERT(!it->empty());
    CT_ASSERT_EQUAL(luid, *it);
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());

    // Now make changes via source B directly: these changes are not to be
    // reported back. Google CalDAV is very strict about UID/SEQUENCE,
    // work around that by not reusing a UID inside this test.
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    // add
    std::string mangled = config.m_mangleItem(config.m_insertItem, false, "-B");
    SyncSourceRaw::InsertItemResult res;
    SOURCE_ASSERT_NO_FAILURE(source.get(), res = source->insertItemRaw("", mangled));
    CT_ASSERT(!res.m_luid.empty());
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());
    // update
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
    mangled = config.m_mangleItem(config.m_updateItem, false, "-B");
    SOURCE_ASSERT_NO_FAILURE(source.get(), res = source->insertItemRaw(res.m_luid, mangled));
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());
    // delete
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
    SOURCE_ASSERT_NO_FAILURE(source.get(), source->deleteItem(res.m_luid));
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());

    CLIENT_TEST_LOG("insert another item via source A");
    CT_ASSERT_NO_THROW(insert(createSourceA, config.m_insertItem, false, nullptr, "-C"));
    CLIENT_TEST_LOG("check for new item via source B");
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countNewItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
    SOURCE_ASSERT_NO_FAILURE(source.get(), it = source->getAllItems().begin());
    CT_ASSERT(it != source->getAllItems().end());
    luid = *it;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source->readItem(*it, item));
    string newItem;
    SOURCE_ASSERT_NO_FAILURE(source.get(), it = source->getNewItems().begin());
    CT_ASSERT(it != source->getNewItems().end());
    SOURCE_ASSERT_NO_FAILURE(source.get(), source->readItem(*it, item));
    CT_ASSERT_EQUAL(luid, *it);
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());

    CLIENT_TEST_LOG("update item via source A");
    CT_ASSERT_NO_THROW(update(createSourceA, config.m_updateItem, "-C"));
    CLIENT_TEST_LOG("check for updated item via source B");
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countUpdatedItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
    string updatedItem;
    SOURCE_ASSERT_NO_FAILURE(source.get(), it = source->getUpdatedItems().begin());
    CT_ASSERT(it != source->getUpdatedItems().end());
    SOURCE_ASSERT_NO_FAILURE(source.get(), source->readItem(*it, updatedItem));
    CT_ASSERT_EQUAL(luid, *it);
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());

    CLIENT_TEST_LOG("one item, no changes in source B");
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());

    CLIENT_TEST_LOG("start anew in both sources");
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());
    CLIENT_TEST_LOG("create and update an item in source A");
    // Must use a UID different than the one used before despite that data being gone,
    // to keep Google CalDAV server happy.
    CT_ASSERT_NO_THROW(insert(createSourceA, config.m_insertItem, false, nullptr, "-D"));
    CT_ASSERT_NO_THROW(update(createSourceA, config.m_updateItem, "-D"));
    CLIENT_TEST_LOG("should only be listed as new or updated in source B, but not both");
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countNewItems(source.get()) + countUpdatedItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());

    CLIENT_TEST_LOG("start anew once more in both sources");
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    CT_ASSERT_NO_THROW(restart ? source.stopAccess() : source.reset());
    CLIENT_TEST_LOG("create, delete and recreate an item in source A");
    CT_ASSERT_NO_THROW(insert(createSourceA, config.m_insertItem, false, nullptr, "-E"));
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    CT_ASSERT_NO_THROW(insert(createSourceA, config.m_insertItem, false, nullptr, "-F"));
    CLIENT_TEST_LOG("should only be listed as new or updated in source B, even if\n "
                    "(as for calendar with UID) the same LUID gets reused");
    SOURCE_ASSERT_NO_FAILURE(source.get(), restart ? source.startAccess() : source.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
    SOURCE_ASSERT_EQUAL(source.get(), 1, countNewItems(source.get()) + countUpdatedItems(source.get()));
    if (countDeletedItems(source.get()) == 1) {
        // It's not nice, but acceptable to send the LUID of a deleted item to a
        // server which has never seen that LUID. The LUID must not be the same as
        // the one we list as new or updated, though.
        SOURCE_ASSERT_NO_FAILURE(source.get(), it = source->getDeletedItems().begin());
        CT_ASSERT(it != source->getDeletedItems().end());
        SOURCE_ASSERT_NO_FAILURE(source.get(), it2 = source->getNewItems().begin());
        if (it2 == source->getNewItems().end()) {
            SOURCE_ASSERT_NO_FAILURE(source.get(), it2 = source->getUpdatedItems().begin());
            CT_ASSERT(it2 != source->getUpdatedItems().end());
        }
        CT_ASSERT(*it != *it2);
    } else {
        SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
    }

    CT_ASSERT_NO_THROW(source.reset());
}

// complex sequence of changes, with source B instantiated anew
// after each change
void LocalTests::testChanges()
{
    doChanges(false);
}

// complex sequence of changes, with source B only instantiated once
// and restarted multiple times
void LocalTests::testChangesMultiCycles()
{
    doChanges(true);
}

// Make changes in one source and verify that other linked
// sources do not see and report any changes in their view
// of the shared database. Source A of the other sources
// is created once and is restarted, source B is created
// from scratch after each change.
void LocalTests::testLinkedSources()
{
    // make changes in each of the sources (doesn't have t be
    // the current one)
    for (LocalTests *main: m_linkedSources) {
        CLIENT_TEST_LOG("making changes in %s", main->getSourceName().c_str());

        // first delete via *all* sources
        for (LocalTests *test: m_linkedSources) {
            CLIENT_TEST_LOG("clean via source A of %s", test->getSourceName().c_str());
            CT_ASSERT_NO_THROW(test->deleteAll(test->createSourceA));
            // reset change tracking in source B
            TestingSyncSourcePtr sourceB;
            SOURCE_ASSERT_NO_FAILURE(sourceB.get(), sourceB.reset(test->createSourceB()));
        }

        std::map<std::string, TestingSyncSourcePtr> sourcesA;
        for (LocalTests *test: m_linkedSources) {
            if (test == main) {
                continue;
            }
            TestingSyncSourcePtr &source = sourcesA[test->getSourceName()];
            CLIENT_TEST_LOG("creating source A of %s", test->getSourceName().c_str());
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(test->createSourceA()));
            CT_ASSERT_NO_THROW(source.stopAccess());
        }

        // insert one item
        CLIENT_TEST_LOG("inserting into %s", main->getSourceName().c_str());
        CT_ASSERT_NO_THROW(main->doInsert());
        for (LocalTests *test: m_linkedSources) {
            if (test == main) {
                continue;
            }
            TestingSyncSourcePtr &source = sourcesA[test->getSourceName()];
            CLIENT_TEST_LOG("checking %s after insertion into %s",
                            test->getSourceName().c_str(),
                            main->getSourceName().c_str());
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.startAccess());
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.stopAccess());

            TestingSyncSourcePtr sourceB;
            SOURCE_ASSERT_NO_FAILURE(sourceB.get(), sourceB.reset(test->createSourceB()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countItems(sourceB.get()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countNewItems(sourceB.get()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countUpdatedItems(sourceB.get()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countDeletedItems(sourceB.get()));
        }

        // update one item
        CLIENT_TEST_LOG("updating in %s", main->getSourceName().c_str());
        CT_ASSERT_NO_THROW(main->update(main->createSourceA, main->config.m_updateItem));
        for (LocalTests *test: m_linkedSources) {
            if (test == main) {
                continue;
            }
            TestingSyncSourcePtr &source = sourcesA[test->getSourceName()];
            CLIENT_TEST_LOG("checking %s after update into %s",
                            test->getSourceName().c_str(),
                            main->getSourceName().c_str());
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.startAccess());
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.stopAccess());

            TestingSyncSourcePtr sourceB;
            SOURCE_ASSERT_NO_FAILURE(sourceB.get(), sourceB.reset(test->createSourceB()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countItems(sourceB.get()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countNewItems(sourceB.get()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countUpdatedItems(sourceB.get()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countDeletedItems(sourceB.get()));
        }

        // delete one item
        CLIENT_TEST_LOG("deleting in %s", main->getSourceName().c_str());
        CT_ASSERT_NO_THROW(main->deleteAll(main->createSourceA));
        for (LocalTests *test: m_linkedSources) {
            if (test == main) {
                continue;
            }
            TestingSyncSourcePtr &source = sourcesA[test->getSourceName()];
            CLIENT_TEST_LOG("checking %s after delete in %s",
                            test->getSourceName().c_str(),
                            main->getSourceName().c_str());
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.startAccess());
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.stopAccess());

            TestingSyncSourcePtr sourceB;
            SOURCE_ASSERT_NO_FAILURE(sourceB.get(), sourceB.reset(test->createSourceB()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countItems(sourceB.get()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countNewItems(sourceB.get()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countUpdatedItems(sourceB.get()));
            SOURCE_ASSERT_EQUAL(sourceB.get(), 0, countDeletedItems(sourceB.get()));
        }
    }
}

// clean database, import file, then export again and compare
void LocalTests::doImport(const std::string &testcases) {
    // check additional requirements
    CT_ASSERT(config.m_import);
    CT_ASSERT(config.m_dump);
    CT_ASSERT(config.m_compare);
    CT_ASSERT(!testcases.empty());
    CT_ASSERT(config.m_createSourceA);

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    // import via sync source A
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
    restoreStorage(config, client);
    std::string actualData;
    std::string importFailures = config.m_import(client, *source.get(), config, testcases, actualData, nullptr);
    backupStorage(config, client);
    CT_ASSERT_NO_THROW(source.reset());

    // export again and compare against original file,
    // without relying on change tracking (because
    // Google ActiveSync has problems with Fetch,
    // which would be needed for a data dump whenr
    // using the incremental approach)
    TestingSyncSourcePtr copy;
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceA(), TestingSyncSourcePtr::SLOW));
    bool equal = compareDatabases(actualData.c_str(), *copy.get(), false);
    CT_ASSERT_NO_THROW(source.reset());

    if (importFailures.empty()) {
        CT_ASSERT_MESSAGE("imported and exported data equal", equal);
    } else {
        CT_ASSERT_EQUAL(std::string(""), importFailures);
    }
}

void LocalTests::testImport() {
    doImport(config.m_testcases);
}

// same as testImport() with immediate delete
void LocalTests::testImportDelete() {
    CT_ASSERT_NO_THROW(testImport());

    // delete again, because it was observed that this did not
    // work right with calendars in SyncEvolution
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
}

// clean database, import file, update with minimized test data (= all
// non-essential properties removed), compare: verifies that updates
// can remove data
void LocalTests::testRemoveProperties() {
    // check additional requirements
    CT_ASSERT(config.m_import);
    CT_ASSERT(config.m_dump);
    CT_ASSERT(config.m_compare);
    CT_ASSERT(!config.m_testcases.empty());
    CT_ASSERT(config.m_createSourceA);

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    // import via sync source A
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
    restoreStorage(config, client);
    std::string testcases;
    std::list<std::string> luids;
    std::string importFailures = config.m_import(client, *source.get(), config, config.m_testcases, testcases, &luids);
    backupStorage(config, client);
    CT_ASSERT_NO_THROW(source.reset());

    // don't check for correct importing - that is done in testImport

    // reduce data
    std::list<std::string> items;
    std::string dummy;
    CT_ASSERT_NO_THROW(ClientTest::getItems(testcases, items, dummy));
    std::string updated = getCurrentTest();
    updated += ".updated.";
    updated += config.m_sourceName;
    updated += ".dat";
    simplifyFilename(updated);
    ofstream out(updated.c_str());

    for (std::string &item: items) {
        std::smatch match;
        static const std::regex bodyre(R"del(\bBEGIN:(VCARD|VEVENT|VTODO|VJOURNAL)\r?\n([\s\S]*)\b(END:\1))del");
        CT_ASSERT(std::regex_search(item, match, bodyre));
        auto body = match[2];

        static const std::regex propre(R"del(((\S[^;:]*).*\r?\n(?:\s.*\r?\n)*))del");
        std::list<std::string> result;
        for (auto it = std::sregex_iterator(body.first, body.second, propre);
             it != std::sregex_iterator();
             ++it) {
            auto match = *it;
            auto prop = match[1];
            auto propname = match[2];
            if (config.m_essentialProperties.find(propname) != config.m_essentialProperties.end()) {
                result.push_back(prop);
            }
        }

        item.replace(std::distance(item.cbegin(), body.first), body.length(),
                     boost::join(result, ""));
        out << item << "\n";
    }
    out.close();

    // update
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
    std::string updateFailures = config.m_import(client, *source.get(), config, updated, dummy, &luids);
    CT_ASSERT_NO_THROW(source.reset());

    // compare
    TestingSyncSourcePtr copy;
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceA(), TestingSyncSourcePtr::SLOW));
    std::unique_ptr<ScopedEnvChange> envProps;
    if (currentServer() == "googlecontacts") {
        // Google CardDAV server does not remove X- properties when
        // they are not sent at all. TODO (?): send them as empty
        // properties.
        envProps.reset(new ScopedEnvChange("CLIENT_TEST_STRIP_PROPERTIES", "(X-.*)"));
    }
    bool equal = compareDatabases(updated.c_str(), *copy.get(), false);
    CT_ASSERT_NO_THROW(source.reset());

    if (importFailures.empty() && updateFailures.empty()) {
        CT_ASSERT_MESSAGE("imported and exported data equal", equal);
    } else {
        CT_ASSERT_EQUAL(std::string(""), importFailures + updateFailures);
    }
}

// test change tracking with large number of items
void LocalTests::testManyChanges() {
    // check additional requirements
    CT_ASSERT(!config.m_templateItem.empty());

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    // check that everything is empty, also resets change counter of sync source B
    TestingSyncSourcePtr copy;
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // now insert plenty of items
    int numItems;
    CT_ASSERT_NO_THROW(numItems = insertManyItems(createSourceA).size());

    // check that exactly this number of items is listed as new
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), numItems, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), numItems, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // delete all items
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    // verify again
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), numItems, countDeletedItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());
}

template<class T, class V> int countEqual(const T &container,
                                          const V &value) {
    return count(container.begin(),
                 container.end(),
                 value);
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsParent() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // now insert main item
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false, &parentData));

    // check that exactly the parent is listed as new
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    // delete all items
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    // verify again
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), parent));
    }
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsChild() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // same as above for child item
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), child));
    }
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsParentChild() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // insert parent first, then child
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false, &parentData));
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (config.m_supportsReccurenceEXDates) {
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
        CLIENT_TEST_LOG("retrieve parent as reported to the Synthesis engine, check for X-SYNCEVOLUTION-EXDATE-DETACHED");
        std::string parentDataEngine;
        CT_ASSERT_NO_THROW(source->readItem(parent, parentDataEngine));
        size_t pos = childData.find("RECURRENCE-ID");
        CT_ASSERT(pos != childData.npos);
        size_t end = childData.find_first_of("\r\n", pos);
        CT_ASSERT(end != childData.npos);
        std::string exdate = childData.substr(pos, end - pos);
        boost::replace_first(exdate, "RECURRENCE-ID", "X-SYNCEVOLUTION-EXDATE-DETACHED");
        // not generated because not needed by Synthesis engine
        boost::replace_first(exdate, ";VALUE=DATE", "");
        pos = parentDataEngine.find(exdate);
        CT_ASSERT_MESSAGE(exdate + " not found in:\n" + parentDataEngine, pos != parentDataEngine.npos);
    }

    if (config.m_supportsReccurenceEXDates) {
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
        CLIENT_TEST_LOG("retrieve parent as reported to the Synthesis engine, check for X-SYNCEVOLUTION-EXDATE-DETACHED");
        std::string parentDataEngine;
        CT_ASSERT_NO_THROW(source->readItem(parent, parentDataEngine));
        size_t pos = childData.find("RECURRENCE-ID");
        CT_ASSERT(pos != childData.npos);
        size_t end = childData.find_first_of("\r\n", pos);
        CT_ASSERT(end != childData.npos);
        std::string exdate = childData.substr(pos, end - pos);
        boost::replace_first(exdate, "RECURRENCE-ID", "X-SYNCEVOLUTION-EXDATE-DETACHED");
        // not generated because not needed by Synthesis engine
        boost::replace_first(exdate, ";VALUE=DATE", "");
        pos = parentDataEngine.find(exdate);
        CT_ASSERT_MESSAGE(exdate + " not found in:\n" + parentDataEngine, pos != parentDataEngine.npos);
    }

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), child));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), parent));
    }
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsChildParent() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // insert child first, then parent
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &parentData));
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], true, &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), child));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), parent));
    }
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsChildChangesParent() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // insert child first, check changes, then insert the parent
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], true, &parentData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countNewItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listNewItems(copy.get()), parent));
    }
    // relaxed semantic: the child item might be considered updated now if
    // it had to be modified when inserting the parent
    SOURCE_ASSERT(copy.get(), 1 >= countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), child));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), parent));
    }
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsRemoveParentFirst() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // insert both items, remove parent, then child
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false, &parentData));
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    CT_ASSERT_NO_THROW(deleteItem(createSourceA, parent));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    // deleting the parent may or may not modify the child
    SOURCE_ASSERT(copy.get(), 1 >= countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteItem(createSourceA, child));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), child));
    }
    CT_ASSERT_NO_THROW(copy.reset());
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsRemoveNormal() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr source, copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // insert both items, remove child, then parent
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false, &parentData));
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    CT_ASSERT_NO_THROW(deleteItem(createSourceA, child));

    // The removal of the child fails with Exchange (BMC #22849).
    // Skip the testing, proceed to full removal.
    if (currentServer() != "exchange") {
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
        if (getCurrentTest().find("::eds_event::") != std::string::npos) {
            bool usingEDSLT36;

#ifdef  EVOLUTION_COMPATIBILITY
            // Need to check at runtime.
            // We are using EDS < 3.6 if (and only if) we found the right libs.
            usingEDSLT36 = EDSAbiHaveEcal;
#elif defined(USE_EDS_CLIENT)
            // Detected at runtime: new EDS API.
            usingEDSLT36 = false;
#else
            // Detected at runtime: old EDS API.
            usingEDSLT36 = true;
#endif
            if (usingEDSLT36) {
                // hack: ignore EDS < 3.6 side effect of adding EXDATE to parent, see http://bugs.meego.com/show_bug.cgi?id=10906
                size_t pos = parentData.rfind("DTSTART");
                parentData.insert(pos,
                                  getCurrentTest().find("LinkedItemsAllDay") == std::string::npos ?
                                  "EXDATE:20080413T090000\n" :
                                  "EXDATE:20080413\n");
            }
        }

        CT_ASSERT_NO_THROW(compareDatabases(source.get(), &parentData, nullptr));
        SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
        SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
        SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
        SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));

        CT_ASSERT_NO_THROW(source.reset());

        SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
        SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
        // parent might have been updated
        int updated = false;
        CT_ASSERT_NO_THROW(updated = countUpdatedItems(copy.get()));
        SOURCE_ASSERT(copy.get(), 0 <= updated && updated <= 1);
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countDeletedItems(copy.get()));
        if (!config.m_sourceLUIDsAreVolatile) {
            SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), child));
        }
        CT_ASSERT_NO_THROW(copy.reset());
    }

    CT_ASSERT_NO_THROW(deleteItem(createSourceA, parent));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(),
                        // Exchange did not actually remove child above, done now.
                        currentServer() != "exchange" ? 1 : 2,
                        countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsInsertParentTwice() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // add parent twice (should be turned into update)
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false, &parentData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false, &parentData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listUpdatedItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteItem(createSourceA, parent));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), parent));
    }
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsInsertChildTwice() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // add child twice (should be turned into update)
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1]));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listUpdatedItems(copy.get()), child));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteItem(createSourceA, child));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), child));
    }
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsParentUpdate() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // add parent, then update it
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false, &parentData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    CT_ASSERT_NO_THROW(parent = updateItem(createSourceA, config, parent, items[0], &parentData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listUpdatedItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteItem(createSourceA, parent));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsUpdateChild() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // add child, then update it
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    CT_ASSERT_NO_THROW(child = updateItem(createSourceA, config, child, items[1], &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listUpdatedItems(copy.get()), child));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteItem(createSourceA, child));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 1, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), child));
    }
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsInsertBothUpdateChild() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // add parent and child, then update child
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false, &parentData));
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    CT_ASSERT_NO_THROW(child = updateItem(createSourceA, config, child, items[1], &childData));

    // child has to be listed as modified, parent may be
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT(copy.get(), 1 <= countUpdatedItems(copy.get()));
    SOURCE_ASSERT(copy.get(), 2 >= countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listUpdatedItems(copy.get()), child));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteItem(createSourceA, parent));
    CT_ASSERT_NO_THROW(deleteItem(createSourceA, child));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), parent));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), child));
    }
    CT_ASSERT_NO_THROW(copy.reset());
}

// test inserting, removing and updating of parent + child item in
// various order plus change tracking
void LocalTests::testLinkedItemsInsertBothUpdateParent() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // check that everything is empty, also resets change counter of sync source B
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // add parent and child, then update parent
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false, &parentData));
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), child));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    CT_ASSERT_NO_THROW(parent = updateItem(createSourceA, config, parent, items[0], &parentData));

    // parent has to be listed as modified, child may be
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, &childData, nullptr));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT(copy.get(), 1 <= countUpdatedItems(copy.get()));
    SOURCE_ASSERT(copy.get(), 2 >= countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listUpdatedItems(copy.get()), parent));
    }
    CT_ASSERT_NO_THROW(copy.reset());

    if (getenv("CLIENT_TEST_LINKED_ITEMS_NO_DELETE")) {
        return;
    }

    CT_ASSERT_NO_THROW(deleteItem(createSourceA, parent));
    CT_ASSERT_NO_THROW(deleteItem(createSourceA, child));

    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countNewItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countUpdatedItems(copy.get()));
    SOURCE_ASSERT_EQUAL(copy.get(), 2, countDeletedItems(copy.get()));
    if (!config.m_sourceLUIDsAreVolatile) {
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), parent));
        SOURCE_ASSERT_EQUAL(copy.get(), 1, countEqual(listDeletedItems(copy.get()), child));
    }
}

// - insert parent and child
// - update child *without* UID and RECURRENCE-ID: source expected to  re-insert them
void LocalTests::testLinkedItemsInsertBothUpdateChildNoIDs() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;
    std::string parentData, childData;
    TestingSyncSourcePtr copy;

    // add parent and child, then update child
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false, &parentData));
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    // remove UID and RECURRENCE-ID before updating
    std::string reducedChildData = items[1];
    std::string uid;
    CT_ASSERT_NO_THROW(uid = stripProperty(reducedChildData, "UID"));
    std::string rid;
    CT_ASSERT_NO_THROW(rid = stripProperty(reducedChildData, "RECURRENCE-ID"));
    CT_ASSERT_NO_THROW(child = updateItem(createSourceA, config, child, reducedChildData, &childData));

    // compare
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceA()));
    CT_ASSERT_NO_THROW(insertProperty(childData, uid, "END:VEVENT"));
    CT_ASSERT_NO_THROW(insertProperty(childData, rid, "END:VEVENT"));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &parentData, &childData, nullptr));
}

// - insert child
// - update child *without* UID and RECURRENCE-ID: source expected to re-insert them
void LocalTests::testLinkedItemsUpdateChildNoIDs() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string child;
    std::string childData;
    TestingSyncSourcePtr copy;

    // add child, then update child
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false, &childData));

    // remove UID and RECURRENCE-ID before updating
    std::string reducedChildData = items[1];
    std::string uid;
    CT_ASSERT_NO_THROW(uid = stripProperty(reducedChildData, "UID"));
    std::string rid;
    CT_ASSERT_NO_THROW(rid = stripProperty(reducedChildData, "RECURRENCE-ID"));
    CT_ASSERT_NO_THROW(child = updateItem(createSourceA, config, child, reducedChildData, &childData));

    // compare
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceA()));
    CT_ASSERT_NO_THROW(insertProperty(childData, uid, "END:VEVENT"));
    CT_ASSERT_NO_THROW(insertProperty(childData, rid, "END:VEVENT"));
    CT_ASSERT_NO_THROW(compareDatabases(copy.get(), &childData, nullptr));
}

// insert parent, try to delete or retrieve non-existent child:
// must report 404
void LocalTests::testLinkedItemsSingle404() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;

    // now insert main item
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false));

    // fake subid: works for CalDAV and EDS
    child = parent + "no-such-subitem";

    // read
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset((createSourceA())));
    SyncMLStatus status = STATUS_OK;
    CT_ASSERT_NO_THROW(try {
            std::string data;
            source->readItem(child, data);
        } catch (const StatusException &ex) {
            status = ex.syncMLStatus();
        }
    );
    CT_ASSERT_EQUAL(STATUS_NOT_FOUND, status);

    // delete
    status = STATUS_OK;
    CT_ASSERT_NO_THROW(try {
            source->deleteItem(child);
        } catch (const StatusException &ex) {
            status = ex.syncMLStatus();
        }
    );
    CT_ASSERT_EQUAL(STATUS_NOT_FOUND, status);
}

// insert parent and child, try to delete or retrieve non-existent child:
// must report 404
void LocalTests::testLinkedItemsMany404() {
    ClientTestConfig::LinkedItems_t items = getParentChildData();

    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    std::string parent, child;

    // now insert two items
    CT_ASSERT_NO_THROW(parent = insert(createSourceA, items[0], false));
    CT_ASSERT_NO_THROW(child = insert(createSourceA, items[1], false));

    // fake subid: works for CalDAV and EDS
    child = parent + "no-such-subitem";

    // read
    TestingSyncSourcePtr source;
    SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(createSourceA()));
    SyncMLStatus status = STATUS_OK;
    CT_ASSERT_NO_THROW(try {
            std::string data;
            source->readItem(child, data);
        } catch (const StatusException &ex) {
            status = ex.syncMLStatus();
        }
    );
    CT_ASSERT_EQUAL(STATUS_NOT_FOUND, status);

    // delete
    status = STATUS_OK;
    CT_ASSERT_NO_THROW(try {
            source->deleteItem(child);
        } catch (const StatusException &ex) {
            status = ex.syncMLStatus();
        }
    );
    CT_ASSERT_EQUAL(STATUS_NOT_FOUND, status);
}

// Is run as Client::Source::LinkedItems<testdata>::testSubsetStart<start>Skip<skip>
// where start = first detached recurrence to send and skip = detached recurrences
// to skip before adding the next one (=> 0 = send all).
//
// "Exdate" instead of Skip<skip> is special: it picks the <start>, <start> + 1 and last
// item, which typically leads to an irregular pattern and requires adding EXDATEs
// in the activesyncd.
void LocalTests::testSubset()
{
    ClientTestConfig::LinkedItems_t items = getParentChildData();
    std::string test = getCurrentTest();
    static const std::regex re(R"del(testSubsetStart(\d+)(?:Skip(\d+)|(Exdate)))del");
    std::smatch match;
    CT_ASSERT(std::regex_search(test, match, re));
    int start = atoi(match[1].str().c_str());
    int skip;
    auto optSkip = match[2];
    auto exdate = match[3];
    if (!exdate.matched) {
        // skip case
        CT_ASSERT(optSkip.length());
        skip = atoi(optSkip.str().c_str());
    } else {
        // EXDATE case
        CT_ASSERT_EQUAL(std::string("Exdate"), exdate.str());
        skip = -1;
    }
    CT_ASSERT(items.size() > (size_t)start);
    CT_ASSERT(skip >= -1);

    // check that everything is empty, also resets change counter of sync source B
    CT_ASSERT_NO_THROW(deleteAll(createSourceA));
    TestingSyncSourcePtr copy;
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
    SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
    CT_ASSERT_NO_THROW(copy.reset());

    // insert parent first, then child
    std::list<std::string> sent;
    int i = start;
    while ((size_t)i < items.size() &&
           ((start == 0 && skip == 0) || /* _0_0 really uses all items (stress test) */
            skip == -1 ||                /* _x_e already is limited to 3 items */
            i - start < 5)) {            /* avoid huge number of items per test */
        std::string data;
        std::string message = StringPrintf("start %d, skip %d, at %d of %d",
                                           start, skip, i, (int)items.size());
        CT_ASSERT_NO_THROW_MESSAGE(message, insert(createSourceA, items[i], false, &data));
        sent.push_back(data);

        SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(createSourceB()));
        std::list<std::string> actual(sent);
        if (items.m_testLinkedItemsSubsetAdditional) {
            std::string event = items.m_testLinkedItemsSubsetAdditional(start, skip, i, items.size());
            if (!event.empty()) {
                actual.push_back(event);
            }
        }
        CT_ASSERT_NO_THROW_MESSAGE(message, compareDatabasesRef(*copy, actual));

        if (skip >= 0) {
            // skip intermediate items
            i += skip + 1;
        } else if (i == start) {
            // go to second item
            i++;
        } else if (i == start + 1) {
            // go to last item
            CT_ASSERT((size_t)i != items.size() - 1);
            i = items.size() - 1;
        } else {
            // done with first, second and last item
            break;
        }
    }
}


ClientTestConfig::LinkedItems_t LocalTests::getParentChildData()
{
    // extract suffix and use it as index for our config
    std::string test = getCurrentTest();
    const std::string testname = "LinkedItems";
    size_t off = test.find(testname);
    CT_ASSERT(off != test.npos);
    off += testname.size();
    size_t end = test.find(':', off);
    CT_ASSERT(end != test.npos);
    std::string name = test.substr(off, end - off);
    for (const ClientTestConfig::LinkedItems_t &items: config.m_linkedItems) {
        if (items.m_name == name) {
            return items;
        }
    }
    for (const ClientTestConfig::LinkedItems_t &items: config.m_linkedItemsSubset) {
        if (items.m_name == name) {
            return items;
        }
    }
    CT_ASSERT_MESSAGE("linked items test data not found", false);
    return ClientTestConfig::LinkedItems_t();
}

SyncTests::SyncTests(const std::string &name, ClientTest &cl, std::vector<int> sourceIndices, bool isClientA) :
    CppUnit::TestSuite(name),
    client(cl) {
    sourceArray = new int[sourceIndices.size() + 1];
    int offset = 0;
    for (auto it = sourceIndices.begin();
         it != sourceIndices.end();
         ++it) {
        ClientTest::Config config;
        client.getSyncSourceConfig(*it, config);

        if (!config.m_sourceName.empty()) {
            sourceArray[sources.size()+offset] = *it;
            if (!config.m_subConfigs.empty()) {
                vector<string> subs;
                boost::split (subs, config.m_subConfigs, boost::is_any_of(","));
                offset++;
                ClientTest::Config subConfig;
                for (string sub: subs) {
                client.getSourceConfig (sub, subConfig);
                sources.push_back(std::pair<int,LocalTests *>(*it, cl.createLocalTests(sub, client.getLocalSourcePosition(sub), subConfig)));
                offset--;
                }
            } else {
                sources.push_back(std::pair<int,LocalTests *>(*it, cl.createLocalTests(config.m_sourceName, client.getLocalSourcePosition(config.m_sourceName), config)));
            }
        }
    }
    sourceArray[sources.size()+ offset] = -1;

    // check whether we have a second client
    ClientTest *clientB = cl.getClientB();
    if (clientB) {
        accessClientB = clientB->createSyncTests(name, sourceIndices, false);
    } else {
        accessClientB = 0;
    }
}

SyncTests::~SyncTests() {
    for (source_it it = sources.begin();
         it != sources.end();
         ++it) {
        delete it->second;
    }
    delete [] sourceArray;
    if (accessClientB) {
        delete accessClientB;
    }
}

/** adds the supported tests to the instance itself */
void SyncTests::addTests(bool isFirstSource) {
    if (sources.size()) {
        const ClientTest::Config &config(sources[0].second->config);

        // run this test first, even if it is more complex:
        // if it works, all the following tests will run with
        // the server in a deterministic state
        if (config.m_createSourceA) {
            if (!config.m_insertItem.empty()) {
                ADD_TEST(SyncTests, testDeleteAllRefresh);
            }
        }

        ADD_TEST(SyncTests, testTwoWaySync);
        ADD_TEST(SyncTests, testSlowSync);
        ADD_TEST(SyncTests, testRefreshFromServerSync);
        ADD_TEST(SyncTests, testRefreshFromClientSync);
        ADD_TEST(SyncTests, testRefreshFromRemoteSync);
        ADD_TEST(SyncTests, testRefreshFromLocalSync);
        // testTimeout is independent of the actual peer; all it needs
        // is a SyncML client config. Can't test for that explicitly
        // here, so only rule out the test if we run in server mode.
        if (isFirstSource &&
            (!getenv("CLIENT_TEST_MODE") ||
             strcmp(getenv("CLIENT_TEST_MODE"), "server"))) {
            ADD_TEST(SyncTests, testTimeout);
        }

        if (config.m_compare &&
            !config.m_testcases.empty() &&
            !isServerMode()) {
            ADD_TEST(SyncTests, testConversion);
        }

        if (config.m_createSourceA) {
            if (!config.m_insertItem.empty()) {
                ADD_TEST(SyncTests, testRefreshFromServerSemantic);
                ADD_TEST(SyncTests, testRefreshFromClientSemantic);
                ADD_TEST(SyncTests, testRefreshStatus);

                // This test works regardless whether the peer can
                // restart: if restarts are not possible, it checks
                // that they don't occur. The rest of the tests then
                // only make sense when restarting works.
                ADD_TEST(SyncTests, testTwoWayRestart);
                if (getenv("CLIENT_TEST_PEER_CAN_RESTART")) {
                    ADD_TEST(SyncTests, testSlowRestart);
                    ADD_TEST(SyncTests, testRefreshFromLocalRestart);
                    ADD_TEST(SyncTests, testOneWayFromLocalRestart);
                    ADD_TEST(SyncTests, testRefreshFromRemoteRestart);
                    ADD_TEST(SyncTests, testOneWayFromRemoteRestart);
                    ADD_TEST(SyncTests, testManyRestarts);
                }

                if (accessClientB &&
                    config.m_dump &&
                    config.m_compare) {
                    ADD_TEST(SyncTests, testCopy);
                    ADD_TEST(SyncTests, testDelete);
                    ADD_TEST(SyncTests, testAddUpdate);
                    ADD_TEST(SyncTests, testManyItems);
                    ADD_TEST(SyncTests, testManyDeletes);
                    ADD_TEST(SyncTests, testSlowSyncSemantic);
                    ADD_TEST(SyncTests, testComplexRefreshFromServerSemantic);
                    ADD_TEST(SyncTests, testDeleteBothSides);
                    if (config.m_updateItem.find("UID:") != std::string::npos &&
                        config.m_updateItem.find("LAST-MODIFIED:") != std::string::npos &&
                        sources.size() == 1) {
                        ADD_TEST(SyncTests, testAddBothSides);
                        ADD_TEST(SyncTests, testAddBothSidesRefresh);
                    }

                    // only add when testing individual source,
                    // test data not guaranteed to be available for all sources
                    if (sources.size() == 1 &&
                        !config.m_linkedItems.empty()) {
                        ADD_TEST(SyncTests, testLinkedItemsParentChild);

                        if (config.m_linkedItemsRelaxedSemantic) {
                            ADD_TEST(SyncTests, testLinkedItemsChild);
                            ADD_TEST(SyncTests, testLinkedItemsChildParent);
                        }
                    }

                    if (!config.m_updateItem.empty()) {
                        ADD_TEST(SyncTests, testUpdate);
                    }
                    if (!config.m_complexUpdateItem.empty()) {
                        ADD_TEST(SyncTests, testComplexUpdate);
                    }
                    if (!config.m_mergeItem1.empty() &&
                        !config.m_mergeItem2.empty()) {
                        ADD_TEST(SyncTests, testMerge);
                    }
                    if (config.m_import) {
                        ADD_TEST(SyncTests, testTwinning);
                        ADD_TEST(SyncTests, testItems);
                        ADD_TEST(SyncTests, testItemsXML);
                        if (config.m_update) {
                            ADD_TEST(SyncTests, testExtensions);
                        }
                    }
                    if (!config.m_templateItem.empty()) {
                        ADD_TEST(SyncTests, testMaxMsg);
                        ADD_TEST(SyncTests, testLargeObject);
                        ADD_TEST(SyncTests, testOneWayFromServer);
                        ADD_TEST(SyncTests, testOneWayFromClient);
                        ADD_TEST(SyncTests, testOneWayFromRemote);
                        ADD_TEST(SyncTests, testOneWayFromLocal);
                    }
                }
            }

            // Tests which depend on item manipulation in the peer.
            // These tests get enabled if their testdata is found in
            // testcases/synctests/<server name> and if we are
            // currently testing only a single source. The tests will
            // fail if SyncEvolution was not configured correctly for
            // them (see getPeerConfig()).
            if (sources.size() == 1) {
                const std::string sourceName = config.m_sourceName;
#define ADD_PEER_TEST(_x) if (isDir(getPeerTestdata(sourceName, #_x, ""))) { ADD_TEST(SyncTests, _x); }

                ADD_PEER_TEST(testDownload);
                ADD_PEER_TEST(testUpload);
                ADD_PEER_TEST(testUpdateLocalWins);
                ADD_PEER_TEST(testUpdateRemoteWins);
            }
        }

        if (config.m_retrySync &&
            !config.m_insertItem.empty() &&
            !config.m_updateItem.empty() &&
            accessClientB &&
            config.m_dump &&
            config.m_compare) {
            CppUnit::TestSuite *retryTests = new CppUnit::TestSuite(getName() + "::Retry");
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeClientAdd);
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeClientRemove);
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeClientUpdate);
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeServerAdd);
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeServerRemove);
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeServerUpdate);
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeClientAddBig);
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeClientUpdateBig);
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeServerAddBig);
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeServerUpdateBig);
            ADD_TEST_TO_SUITE(retryTests, SyncTests, testInterruptResumeFull);
            addTest(FilterTest(retryTests));
        }

        if (config.m_suspendSync &&
            !config.m_insertItem.empty() &&
            !config.m_updateItem.empty() &&
            accessClientB &&
            config.m_dump &&
            config.m_compare) {
            CppUnit::TestSuite *suspendTests = new CppUnit::TestSuite(getName() + "::Suspend");
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendClientAdd);
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendClientRemove);
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendClientUpdate);
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendServerAdd);
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendServerRemove);
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendServerUpdate);
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendClientAddBig);
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendClientUpdateBig);
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendServerAddBig);
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendServerUpdateBig);
            ADD_TEST_TO_SUITE(suspendTests, SyncTests, testUserSuspendFull);
            addTest(FilterTest(suspendTests));
        }

        if (config.m_resendSync &&
            !config.m_insertItem.empty() &&
            !config.m_updateItem.empty() &&
            accessClientB &&
            config.m_dump &&
            config.m_compare) {
            CppUnit::TestSuite *resendTests = new CppUnit::TestSuite(getName() + "::Resend");
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendClientAdd);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendClientRemove);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendClientUpdate);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendServerAdd);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendServerRemove);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendServerUpdate);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendFull);
            addTest(FilterTest(resendTests));
        }

        if (getenv("CLIENT_TEST_RESEND_PROXY") &&
            !config.m_insertItem.empty() &&
            !config.m_updateItem.empty() &&
            accessClientB &&
            config.m_dump &&
            config.m_compare) {
            CppUnit::TestSuite *resendTests = new CppUnit::TestSuite(getName() + "::ResendProxy");
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendProxyClientAdd);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendProxyClientRemove);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendProxyClientUpdate);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendProxyServerAdd);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendProxyServerRemove);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendProxyServerUpdate);
            ADD_TEST_TO_SUITE(resendTests, SyncTests, testResendProxyFull);
            addTest(FilterTest(resendTests));
        }
    }
}

bool SyncTests::compareDatabases(const char *refFileBase, bool raiseAssert) {
    source_it it1;
    source_it it2;
    bool equal = true;

    CT_ASSERT(accessClientB);
    for (it1 = sources.begin(), it2 = accessClientB->sources.begin();
         it1 != sources.end() && it2 != accessClientB->sources.end();
         ++it1, ++it2) {
        TestingSyncSourcePtr copy;
        SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(it2->second->createSourceB()));
        if (refFileBase) {
            std::string refFile = refFileBase;
            refFile += it1->second->config.m_sourceName;
            refFile += ".dat";
            simplifyFilename(refFile);
            if (!it1->second->compareDatabases(refFile.c_str(), *copy.get(), raiseAssert)) {
                equal = false;
            }
        } else {
            if (!it1->second->compareDatabases(nullptr, *copy.get(), raiseAssert)) {
                equal = false;
            }
        }
        CT_ASSERT_NO_THROW(copy.reset());
    }
    CT_ASSERT(it1 == sources.end());
    CT_ASSERT(it2 == accessClientB->sources.end());

    CT_ASSERT(!raiseAssert || equal);
    return equal;
}

/** deletes all items locally and on server */
void SyncTests::deleteAll(DeleteAllMode mode) {
    SyncPrefix prefix("deleteall", *this);

    const char *value = getenv ("CLIENT_TEST_DELETE_REFRESH");
    if (value) {
        mode = DELETE_ALL_REFRESH;
    }

    switch(mode) {
     case DELETE_ALL_SYNC:
        // a refresh from server would slightly reduce the amount of data exchanged, but not all servers support it
        CT_ASSERT_NO_THROW(allSourcesDeleteAll());
        doSync(__FILE__, __LINE__, "init", SyncOptions(SYNC_SLOW));
        // now that client and server are in sync, delete locally and sync again
        CT_ASSERT_NO_THROW(allSourcesDeleteAll());
        doSync(__FILE__, __LINE__,
               "twoway",
               SyncOptions(SYNC_TWO_WAY,
                           CheckSyncReport(0,0,0, 0,0,-1, true, SYNC_TWO_WAY)));
        break;
     case DELETE_ALL_REFRESH:
        // delete locally and then tell the server to "copy" the empty databases
        CT_ASSERT_NO_THROW(allSourcesDeleteAll());
        doSync(__FILE__, __LINE__,
               "refreshserver",
               SyncOptions(RefreshFromLocalMode(),
                           CheckSyncReport(0,0,0, 0,0,-1, true, SYNC_REFRESH_FROM_LOCAL)));
        break;
    }
}

/** get both clients in sync with empty server, then copy one item from client A to B */
void SyncTests::doCopy() {
    SyncPrefix copy("copy", *this);

    // check requirements
    CT_ASSERT(accessClientB);

    CT_ASSERT_NO_THROW(deleteAll());
    accessClientB->deleteAll();

    bool allowLocalUpdate = getenv("CLIENT_TEST_MAY_COPY_BACK") != nullptr;

    // insert into first database, copy to server
    CT_ASSERT_NO_THROW(allSourcesInsert());
    doSync(__FILE__, __LINE__,
           "send",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0, allowLocalUpdate? -1 : 0, 0, 1,0,0, true, SYNC_TWO_WAY)));

    // copy into second database
    accessClientB->doSync(__FILE__, __LINE__,
                          "recv",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(1,0,0, 0,0,0, true, SYNC_TWO_WAY)));

    CT_ASSERT_NO_THROW(compareDatabases());
}

/**
 * Replicate server database locally. Works with all servers
 * (including those which do not support refresh sync) because the
 * Synthesis engine will do a local delete + slow sync (by default)
 * and a real refresh sync only when asked to explicitly (Funambol).
 */
void SyncTests::refreshClient(SyncOptions options) {
    doSync(__FILE__, __LINE__,
           "refresh",
           options
           .setSyncMode(SYNC_REFRESH_FROM_REMOTE)
           .setCheckReport(CheckSyncReport(-1,0,-1, 0,0,0, true, SYNC_REFRESH_FROM_REMOTE)));
}


// delete all items, locally and on server using refresh-from-client sync
void SyncTests::testDeleteAllRefresh() {
    // start with clean local data
    CT_ASSERT_NO_THROW(allSourcesDeleteAll());

    // copy something to server first; doesn't matter whether it has the
    // item already or not, as long as it exists there afterwards
    CT_ASSERT_NO_THROW(allSourcesInsert());
    doSync(__FILE__, __LINE__, "insert", SyncOptions(SYNC_SLOW));

    // now ensure we can delete it
    deleteAll(DELETE_ALL_REFRESH);

    // nothing stored locally?
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceA()));
        SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
        CT_ASSERT_NO_THROW(source.reset());
    }

    // make sure server really deleted everything
    doSync(__FILE__, __LINE__,
           "check",
           SyncOptions(SYNC_SLOW,
                       CheckSyncReport(0,0,0, 0,0,0, true, SYNC_SLOW)));
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceA()));
        SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
        CT_ASSERT_NO_THROW(source.reset());
    }
}

// refresh-from-server sync, regardless of peer's role
void SyncTests::testRefreshFromServerSync()
{
    doSync(__FILE__, __LINE__,
           SyncOptions(SYNC_REFRESH_FROM_SERVER,
                       CheckSyncReport(-1,-1,-1, -1,-1,-1, true,
                                       isServerMode() ? SYNC_REFRESH_FROM_LOCAL : SYNC_REFRESH_FROM_REMOTE)));
}

// do a refresh-from-client sync, regardless of peer's role
void SyncTests::testRefreshFromClientSync()
{
    doSync(__FILE__, __LINE__,
           SyncOptions(SYNC_REFRESH_FROM_CLIENT,
                       CheckSyncReport(-1,-1,-1, -1,-1,-1, true,
                                       isServerMode() ? SYNC_REFRESH_FROM_REMOTE : SYNC_REFRESH_FROM_LOCAL)));
}

// do a refresh-from-remote sync, regardless of peer's role
void SyncTests::testRefreshFromRemoteSync()
{
    doSync(__FILE__, __LINE__,
           SyncOptions(SYNC_REFRESH_FROM_REMOTE,
                       CheckSyncReport(-1,-1,-1, -1,-1,-1, true, SYNC_REFRESH_FROM_REMOTE)));
}

// do a refresh-from-local sync, regardless of peer's role
void SyncTests::testRefreshFromLocalSync()
{
    doSync(__FILE__, __LINE__,
           SyncOptions(SYNC_REFRESH_FROM_LOCAL,
                       CheckSyncReport(-1,-1,-1, -1,-1,-1, true, SYNC_REFRESH_FROM_LOCAL)));
}

// delete all items, locally and on server using two-way sync
void SyncTests::testDeleteAllSync()
{
    CT_ASSERT_NO_THROW(deleteAll(DELETE_ALL_SYNC));
}

// test that a refresh sync from an empty server leads to an empty datatbase
// and no changes are sent to server during next two-way sync
void SyncTests::testRefreshFromServerSemantic() {
    // clean client and server
    CT_ASSERT_NO_THROW(deleteAll());

    // insert item, then refresh from empty server
    CT_ASSERT_NO_THROW(allSourcesInsert());
    doSync(__FILE__, __LINE__,
           "refresh",
           SyncOptions(RefreshFromPeerMode(),
                       CheckSyncReport(0,0,-1, 0,0,0, true, SYNC_REFRESH_FROM_REMOTE)));

    // check
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceA()));
        SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
        CT_ASSERT_NO_THROW(source.reset());
    }
    doSync(__FILE__, __LINE__,
           "two-way",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,0,0, true, SYNC_TWO_WAY)));
}

// test that a refresh sync from an empty client leads to an empty datatbase
// and no changes are sent to server during next two-way sync
void SyncTests::testRefreshFromClientSemantic() {
    // clean client and server
    CT_ASSERT_NO_THROW(deleteAll());

    // insert item, send to server
    CT_ASSERT_NO_THROW(allSourcesInsert());
    doSync(__FILE__, __LINE__,
           "send",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 1,0,0, true, SYNC_TWO_WAY)));

    // delete locally
    CT_ASSERT_NO_THROW(allSourcesDeleteAll());

    // refresh from client
    doSync(__FILE__, __LINE__,
           "refresh",
           SyncOptions(RefreshFromLocalMode(),
                       CheckSyncReport(0,0,0, 0,0,0, true, SYNC_REFRESH_FROM_LOCAL)));

    // check
    doSync(__FILE__, __LINE__,
           "check",
           SyncOptions(RefreshFromPeerMode(),
                       CheckSyncReport(0,0,0, 0,0,0, true, SYNC_REFRESH_FROM_REMOTE)));
}

// tests the following sequence of events:
// - insert item
// - delete all items
// - insert one other item
// - refresh from client
// => no items should now be listed as new, updated or deleted for this client during another sync
void SyncTests::testRefreshStatus() {
    CT_ASSERT_NO_THROW(allSourcesInsert(false));
    CT_ASSERT_NO_THROW(allSourcesDeleteAll());
    CT_ASSERT_NO_THROW(allSourcesInsert());
    doSync(__FILE__, __LINE__,
           "refresh-from-client",
           SyncOptions(RefreshFromLocalMode(),
                       CheckSyncReport(0,0,0, -1,-1,-1, /* strictly speaking 1,0,0, but not sure exactly what the server will be told */
                                       true, SYNC_REFRESH_FROM_LOCAL)));
    doSync(__FILE__, __LINE__,
           "two-way",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,0,0, true, SYNC_TWO_WAY)));
}

static void logSyncSourceReport(const SyncSource *source)
{
    CLIENT_TEST_LOG("source %s, start of cycle #%d: local new/mod/del/conflict %d/%d/%d/%d, remote %d/%d/%d/%d, mode %s",
                    source->getName().c_str(),
                    source->getRestarts(),
                    source->getItemStat(SyncSourceReport::ITEM_LOCAL, SyncSourceReport::ITEM_ADDED, SyncSource::ITEM_TOTAL),
                    source->getItemStat(SyncSourceReport::ITEM_LOCAL, SyncSourceReport::ITEM_UPDATED, SyncSource::ITEM_TOTAL),
                    source->getItemStat(SyncSourceReport::ITEM_LOCAL, SyncSourceReport::ITEM_REMOVED, SyncSource::ITEM_TOTAL),
                    source->getItemStat(SyncSourceReport::ITEM_LOCAL, SyncSourceReport::ITEM_ANY, SyncSource::ITEM_REJECT),

                    source->getItemStat(SyncSourceReport::ITEM_REMOTE, SyncSourceReport::ITEM_ADDED, SyncSource::ITEM_TOTAL),
                    source->getItemStat(SyncSourceReport::ITEM_REMOTE, SyncSourceReport::ITEM_UPDATED, SyncSource::ITEM_TOTAL),
                    source->getItemStat(SyncSourceReport::ITEM_REMOTE, SyncSourceReport::ITEM_REMOVED, SyncSource::ITEM_TOTAL),
                    source->getItemStat(SyncSourceReport::ITEM_REMOTE, SyncSourceReport::ITEM_ANY, SyncSource::ITEM_REJECT),

                    PrettyPrintSyncMode(source->getFinalSyncMode()).c_str());
}

/**
 * Helper function, to be used inside a SyncOptions start callback
 * to connect all sources instantiated for a sync with the given
 * pre-operation signal.
 */
template<class W, class M, class S>
bool connectSourceSignal(SyncContext &context,
                         W SyncSource::Operations::*operation,
                         M getSignal,
                         const S &slot)
{
    for (const SyncSource *source: *context.getSources())  {
        ((source->getOperations().*operation).*getSignal)().connect(slot);
    }
    return false;
}

void SyncTests::doRestartSync(SyncMode mode)
{
    CT_ASSERT_NO_THROW(deleteAll());
    int startCount = 0;
    bool needToConnect = true;
    typedef std::map<std::string, SyncSourceReport> Reports_t;
    typedef std::map<int, Reports_t> Cycles_t;
    Cycles_t results;

    // Triggered for every m_startDataRead.
    //
    // It records the current source statistics for later checking and
    // logs it.
    //
    // Also requests a restart at the very beginning, once. Must be
    // done before m_endDataWrite, because then it might be too late
    // to restart.
    auto start = [&results] (SyncSource &source, const char *, const char *) {
        if (results.empty()) {
            CLIENT_TEST_LOG("requesting restart");
            SyncContext::requestAnotherSync();
        }
        results[source.getRestarts()][source.getName()] = source;
        logSyncSourceReport(&source);
        return STATUS_OK;
    };

    // Triggered at the end of each m_endDataWrite.
    //
    // Adds a new item or (in later syncs) updates/deletes
    // it. Because the cycle is other, those changes won't
    // interfere with the cycle. Doing real concurrent
    // changes is something for another tests...
    std::function<SyncSource::Operations::EndDataWrite_t::PostSignal::signature_type> end;
    end = [&startCount, this] (SyncSource &source, OperationExecution, sysync::TSyError, bool, char **) mutable {
        if (++startCount == (int)this->sources.size()) {
            CLIENT_TEST_LOG("inserting one item");
            this->allSourcesInsert(true);
        }
        return STATUS_OK;
    };

    auto setup = [&needToConnect, start, &end] (SyncContext &context, SyncOptions &options) mutable {
        if (needToConnect) {
            needToConnect = false;
            connectSourceSignal(context, &SyncSource::Operations::m_startDataRead, &SyncSource::Operations::StartDataRead_t::getPreSignal, start);
            connectSourceSignal(context,  &SyncSource::Operations::m_endDataWrite, &SyncSource::Operations::EndDataWrite_t::getPostSignal, end);
        }
        return false;
    };

    bool canRestart = getenv("CLIENT_TEST_PEER_CAN_RESTART") != nullptr &&
        !isServerMode();

    CT_ASSERT_NO_THROW(doSync(__FILE__, __LINE__,
                              "add",
                              SyncOptions(mode,
                                          CheckSyncReport(0,
                                                          0,
                                                          // TODO (?): should the item added after the initial refresh-from-remote be deleted in the second cycle?
                                                          // Right now it isn't, because the second sync is
                                                          // a one-way-from-remote.
                                                          mode == SYNC_REFRESH_FROM_REMOTE ? /* 1 */ 0 : 0,

                                                          // nothing transferred when item only exists locally
                                                          // and not transferring to peer
                                                          !canRestart ? 0 :
                                                          (mode == SYNC_ONE_WAY_FROM_REMOTE ||
                                                           mode == SYNC_REFRESH_FROM_REMOTE) ? 0 : 1,
                                                          0,
                                                          0,
                                                          true, mode)
                                          .setRestarts(canRestart ? 1 : 0))
                              .setStartCallback(setup)
                              ));

    // two cycles if restarted, one otherwise
    CT_ASSERT_EQUAL((size_t)(canRestart ? 2 : 1), results.size());

    // nothing transfered before first or second cycle
    for (const auto &cycle: results) {
        CT_ASSERT_EQUAL(sources.size(), cycle.second.size());
        for (const auto &entry: cycle.second) {
            CT_ASSERT_NO_THROW(CheckSyncReport(0,0, 0,
                                               0,0,0)
                               .setRestarts(cycle.first)
                               .check(entry.first, entry.second));
        }
    }

    // one item exists now, in all cases
    // (but see remark about refresh-from-remote!)
    for (auto &source_pair: sources)  {
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(source_pair.second->createSourceA()));
        CT_ASSERT_EQUAL(1, countItems(source.get()));
    }

    if (mode == SYNC_REFRESH_FROM_REMOTE ||
        !canRestart) {
        // Can't continue testing for refresh-from-remote, because the
        // item was never sent to remote and will be gone locally
        // after the next refresh-from-remote (prevents updating and
        // deleting it locally).
        // Without restart support further tests don't make much sense.
        // We already verified above that a restart request was
        // correctly rejected/ignored.
        return;
    }

    // update item while the sync runs
    needToConnect = true;
    startCount = 0;
    results.clear();
    end = [&startCount, this] (SyncSource &source, OperationExecution, sysync::TSyError, bool, char **) mutable {
        if (++startCount == (int)this->sources.size()) {
            CLIENT_TEST_LOG("update one item");
            this->allSourcesUpdate();
        }
        return STATUS_OK;
    };

    CT_ASSERT_NO_THROW(doSync(__FILE__, __LINE__,
                              "update",
                              SyncOptions(mode,
                                          CheckSyncReport(0,0,0,

                                                          // refresh-from-local and slow sync transfer existing item
                                                          // in first cycle anew
                                                          (mode == SYNC_REFRESH_FROM_LOCAL ||
                                                           mode == SYNC_SLOW) ? 1 : 0,
                                                          // nothing transferred when item only exists locally
                                                          // and not transferring to peer
                                                          mode == SYNC_ONE_WAY_FROM_REMOTE ? 0 : 1,
                                                          0,
                                                          true, mode)
                                          .setRestarts(1))
                              .setStartCallback(setup)
                              ));

    // two cycles
    CT_ASSERT_EQUAL((size_t)2, results.size());

    // nothing transfered before first or second cycle
    for (const auto &cycle: results) {
        CLIENT_TEST_LOG("checking cycle #%d", cycle.first);
        CT_ASSERT_EQUAL(sources.size(), cycle.second.size());
        for (const auto &entry: cycle.second) {
            CT_ASSERT_NO_THROW(CheckSyncReport(0,0,0,

                                               // refresh-from-local and slow sync transfer existing item
                                               // in first cycle anew
                                               (cycle.first == 1 &&
                                                (mode == SYNC_REFRESH_FROM_LOCAL ||
                                                 mode == SYNC_SLOW)) ? 1 : 0,
                                               0,0)
                               .setRestarts(cycle.first)
                               .check(entry.first, entry.second));
        }
    }

    // one item exists now, in all cases
    // (but see remark about refresh-from-remote!)
    for (auto &source_pair: sources)  {
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(source_pair.second->createSourceA()));
        CT_ASSERT_EQUAL(1, countItems(source.get()));
    }

    // delete item while the sync runs
    needToConnect = true;
    startCount = 0;
    results.clear();
    end = [&startCount, this] (SyncSource &source, OperationExecution, sysync::TSyError, bool, char **) mutable {
        if (++startCount == (int)this->sources.size()) {
            CLIENT_TEST_LOG("delete one item");
            this->allSourcesDeleteAll();
        }
        return STATUS_OK;
    };

    CT_ASSERT_NO_THROW(doSync(__FILE__, __LINE__,
                              "delete",
                              SyncOptions(mode,
                                          CheckSyncReport(0,0,0,

                                                          // refresh-from-local and slow sync transfer existing item
                                                          // in first cycle anew
                                                          (mode == SYNC_REFRESH_FROM_LOCAL ||
                                                           mode == SYNC_SLOW) ? 1 : 0,
                                                          0,
                                                          // nothing transferred when item only existed locally
                                                          // and not transferring to peer
                                                          mode == SYNC_ONE_WAY_FROM_REMOTE ? 0 : 1,
                                                          true, mode)
                                          .setRestarts(1))
                              .setStartCallback(setup)
                              ));

    // two cycles
    CT_ASSERT_EQUAL((size_t)2, results.size());

    // nothing transfered before first or second cycle
    for (const auto &cycle: results) {
        CT_ASSERT_EQUAL(sources.size(), cycle.second.size());
        for (const auto &entry: cycle.second) {
            CT_ASSERT_NO_THROW(CheckSyncReport(0,0, 0,
                                               // refresh-from-local and slow sync transfer existing item
                                               // in first cycle anew
                                               (cycle.first == 1 &&
                                                (mode == SYNC_REFRESH_FROM_LOCAL ||
                                                 mode == SYNC_SLOW)) ? 1 : 0,
                                               0,0)
                               .setRestarts(cycle.first)
                               .check(entry.first, entry.second));
        }
    }

    // no item exists now, in all cases
    for (auto &source_pair: sources)  {
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(source_pair.second->createSourceA()));
        CT_ASSERT_EQUAL(0, countItems(source.get()));
    }
}

// two-way sync when both sides are empty,
// insert item locally while sync runs, restart
// => one item sent to peer
void SyncTests::testTwoWayRestart()
{
    CT_ASSERT_NO_THROW(doRestartSync(SYNC_TWO_WAY));
}

// slow sync when both sides are empty,
// insert item locally while sync runs, restart
// => one item sent to peer
void SyncTests::testSlowRestart()
{
    CT_ASSERT_NO_THROW(doRestartSync(SYNC_SLOW));
}

// refresh-from-local sync when both sides are empty,
// insert item locally while sync runs, restart
// => one item sent to peer
void SyncTests::testRefreshFromLocalRestart()
{
    CT_ASSERT_NO_THROW(doRestartSync(SYNC_REFRESH_FROM_LOCAL));
}

// one-way-from-local sync when both sides are empty,
// insert item locally while sync runs, restart
// => one item sent to peer
void SyncTests::testOneWayFromLocalRestart()
{
    CT_ASSERT_NO_THROW(doRestartSync(SYNC_ONE_WAY_FROM_LOCAL));
}

// refresh-from-remote sync when both sides are empty,
// insert item locally while sync runs, restart
// => *nothing* sent to peer
void SyncTests::testRefreshFromRemoteRestart()
{
    CT_ASSERT_NO_THROW(doRestartSync(SYNC_REFRESH_FROM_REMOTE));
}

// one-way-from-remote sync when both sides are empty,
// insert item locally while sync runs, restart
// => *nothing* sent to peer
void SyncTests::testOneWayFromRemoteRestart()
{
    CT_ASSERT_NO_THROW(doRestartSync(SYNC_ONE_WAY_FROM_REMOTE));
}

// Start with empty database, refresh peer.
// Then add 1, 2, 4, 8 items in four cycles,
// update them the same way, and finally delete them.
// Results in 12 cycles with different changes and
// one empty, final cycle.
void SyncTests::testManyRestarts()
{
    CT_ASSERT_NO_THROW(deleteAll());
    int startCount = 0;
    bool needToConnect = true;
    typedef std::map<std::string, SyncSourceReport> Reports_t;
    typedef std::map<int, Reports_t> Cycles_t;
    Cycles_t results;
    std::map<int, std::list<std::string> > luids;

    // Triggered for every m_startDataRead.
    //
    // It records the current source statistics for later checking,
    // logs it, and does the item changes.
    auto start = [&startCount, &results, this, &luids] (SyncSource &source, const char *, const char *) mutable {
        if (startCount % this->sources.size() == 0) {
            int count;
            switch (startCount / this->sources.size()) {
            case 0:
            case 1:
            case 2:
            case 3:
                count = 1 << (startCount / this->sources.size()); // 1, 2, 4, 8
                CLIENT_TEST_LOG("insert %d item, restart", count);
                this->allSourcesInsertMany(count, count, luids);
                SyncContext::requestAnotherSync();
                break;
            case 4:
            case 5:
            case 6:
            case 7:
                count = 1 << (startCount / this->sources.size() - 4); // 1, 2, 4, 8
                CLIENT_TEST_LOG("insert %d item, restart", count);
                this->allSourcesUpdateMany(count, count, 1, luids, count - 1);
                SyncContext::requestAnotherSync();
                break;
            case 8:
            case 9:
            case 10:
            case 11:
                count = 1 << (startCount / this->sources.size() - 8); // 1, 2, 4, 8
                CLIENT_TEST_LOG("delete %d item, restart", count);
                this->allSourcesRemoveMany(count, luids, count - 1);
                SyncContext::requestAnotherSync();
                break;
            }
        }
        results[source.getRestarts()][source.getName()] = source;
        logSyncSourceReport(&source);
        ++startCount;
        return STATUS_OK;
    };

    auto setup = [&needToConnect, start] (SyncContext &context, SyncOptions &options) mutable {
        if (needToConnect) {
            needToConnect = false;
            connectSourceSignal(context, &SyncSource::Operations::m_startDataRead, &SyncSource::Operations::StartDataRead_t::getPreSignal, start);
        }
        return false;
    };

    CT_ASSERT_NO_THROW(doSync(__FILE__, __LINE__,
                              SyncOptions(SYNC_TWO_WAY,
                                          CheckSyncReport(0,
                                                          0,
                                                          0,

                                                          15,
                                                          15,
                                                          15,
                                                          true, SYNC_TWO_WAY)
                                          .setRestarts(12))
                              .setStartCallback(setup)
                              ));

    // 13 cycles
    CT_ASSERT_EQUAL((size_t)13, results.size());
    static const int changes[13][3] = {
        {  0,  0,  0 }, // nothing before first cycle
        {  1,  0,  0 }, // result of first cycle
        {  3,  0,  0 }, // statistics are cummulative: first + second
        {  7,  0,  0 },
        { 15,  0,  0 },
        { 15,  1,  0 },
        { 15,  3,  0 },
        { 15,  7,  0 },
        { 15, 15,  0 },
        { 15, 15,  1 },
        { 15, 15,  3 },
        { 15, 15,  7 },
        { 15, 15, 15 }
    };
    for (const auto &cycle: results) {
        CT_ASSERT_EQUAL(sources.size(), cycle.second.size());
        for (const auto &entry: cycle.second) {
            const int *c = changes[cycle.first];
            CLIENT_TEST_LOG("Checking stats before cycle #%d, source %s: expected remote %d/%d/%d",
                            cycle.first, entry.first.c_str(),
                            c[0], c[1], c[2]);
            CT_ASSERT_NO_THROW(CheckSyncReport(0,0,0,
                                               c[0], c[1], c[2])
                               .setRestarts(cycle.first)
                               .check(entry.first, entry.second));
        }
    }

    // no item exists now
    for (auto &source_pair: sources)  {
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(source_pair.second->createSourceA()));
        CT_ASSERT_EQUAL(0, countItems(source.get()));
    }
}

// test that a two-way sync copies an item from one address book into the other
void SyncTests::testCopy()
{
    CT_ASSERT_NO_THROW(doCopy());
    CT_ASSERT_NO_THROW(compareDatabases());
}

// test that a two-way sync copies updates from database to the other client,
// using simple data commonly supported by servers
void SyncTests::testUpdate() {
    CT_ASSERT(sources.begin() != sources.end());
    CT_ASSERT(!sources.begin()->second->config.m_updateItem.empty());

    // setup client A, B and server so that they all contain the same item
    CT_ASSERT_NO_THROW(doCopy());

    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->update(it->second->createSourceA, it->second->config.m_updateItem));
    }

    doSync(__FILE__, __LINE__,
           "update",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,1,0, true, SYNC_TWO_WAY)));
    accessClientB->doSync(__FILE__, __LINE__,
                          "update",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(0,1,0, 0,0,0, true, SYNC_TWO_WAY)));

    CT_ASSERT_NO_THROW(compareDatabases());
}

// test that a two-way sync copies updates from database to the other client,
// using data that some, but not all servers support, like adding a second
// phone number to a contact
void SyncTests::testComplexUpdate() {
    // setup client A, B and server so that they all contain the same item
    CT_ASSERT_NO_THROW(doCopy());

    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        it->second->update(it->second->createSourceA,
                           /* this test might get executed with some sources which have
                              a complex update item while others don't: use the normal update item
                              for them or even just the same item */
                           !it->second->config.m_complexUpdateItem.empty() ? it->second->config.m_complexUpdateItem :
                           !it->second->config.m_updateItem.empty() ? it->second->config.m_updateItem :
                           it->second->config.m_insertItem
                           );
    }

    doSync(__FILE__, __LINE__,
           "update",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,1,0, true, SYNC_TWO_WAY)));
    accessClientB->doSync(__FILE__, __LINE__,
                          "update",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(0,1,0, 0,0,0, true, SYNC_TWO_WAY)));

    CT_ASSERT_NO_THROW(compareDatabases());
}


// test that a two-way sync deletes the copy of an item in the other database
void SyncTests::testDelete() {
    // setup client A, B and server so that they all contain the same item
    CT_ASSERT_NO_THROW(doCopy());

    // delete it on A
    CT_ASSERT_NO_THROW(allSourcesDeleteAll());

    // transfer change from A to server to B
    doSync(__FILE__, __LINE__,
           "delete",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,0,1, true, SYNC_TWO_WAY)));
    accessClientB->doSync(__FILE__, __LINE__,
                          "delete",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(0,0,1, 0,0,0, true, SYNC_TWO_WAY)));

    // check client B: shouldn't have any items now
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        TestingSyncSourcePtr copy;
        SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(it->second->createSourceA()));
        SOURCE_ASSERT_EQUAL(copy.get(), 0, countItems(copy.get()));
        CT_ASSERT_NO_THROW(copy.reset());
    }
}

// test what the server does when it finds that different
// fields of the same item have been modified
void SyncTests::testMerge() {
    // setup client A, B and server so that they all contain the same item
    CT_ASSERT_NO_THROW(doCopy());

    // update in client A
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->update(it->second->createSourceA, it->second->config.m_mergeItem1));
    }

    // update in client B
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->update(it->second->createSourceA, it->second->config.m_mergeItem2));
    }

    // send change to server from client A (no conflict)
    doSync(__FILE__, __LINE__,
           "update",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,1,0, true, SYNC_TWO_WAY)));
    // Now the changes from client B (conflict!).
    // There are several possible outcomes:
    // - client item completely replaces server item
    // - server item completely replaces client item (update on client)
    // - server merges and updates client
    accessClientB->doSync(__FILE__, __LINE__,
                          "conflict",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(-1,-1,-1, -1,-1,-1, true, SYNC_TWO_WAY)));

    // figure out how the conflict during ".conflict" was handled
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        TestingSyncSourcePtr copy;
        SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(it->second->createSourceA()));
        int numItems = 0;
        SOURCE_ASSERT_NO_FAILURE(copy.get(), numItems = countItems(copy.get()));
        CT_ASSERT(numItems >= 1);
        CT_ASSERT(numItems <= 2);
        std::cerr << " \"" << it->second->config.m_sourceName << ": " << (numItems == 1 ? "conflicting items were merged" : "both of the conflicting items were preserved") << "\" ";
        std::cerr.flush();
        CT_ASSERT_NO_THROW(copy.reset());        
    }

    // now pull the same changes into client A
    doSync(__FILE__, __LINE__,
           "refresh",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(-1,-1,-1, 0,0,0, true, SYNC_TWO_WAY)));

    // client A and B should have identical data now
    CT_ASSERT_NO_THROW(compareDatabases());

    // Furthermore, it should be identical with the server.
    // Be extra careful and pull that data anew and compare once more.
    doSync(__FILE__, __LINE__,
           "check",
           SyncOptions(RefreshFromPeerMode(),
                       CheckSyncReport(-1,-1,-1, -1,-1,-1, true, SYNC_REFRESH_FROM_REMOTE)));
    CT_ASSERT_NO_THROW(compareDatabases());
}

// test what the server does when it has to execute a slow sync
// with identical data on client and server:
// expected behaviour is that nothing changes
void SyncTests::testTwinning() {
    // clean server and client A
    CT_ASSERT_NO_THROW(deleteAll());

    // import test data
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->testImport());
    }

    // send to server
    doSync(__FILE__, __LINE__, "send", SyncOptions(SYNC_TWO_WAY));

    // ensure that client has the same data, thus ignoring data conversion
    // issues (those are covered by testItems())
    CT_ASSERT_NO_THROW(refreshClient());

    // copy to client B to have another copy
    CT_ASSERT_NO_THROW(accessClientB->refreshClient());

    // slow sync should not change anything
    doSync(__FILE__, __LINE__, "twinning", SyncOptions(SYNC_SLOW));

    // check
    CT_ASSERT_NO_THROW(compareDatabases());
}

// tests one-way sync from peer:
// - get both clients and server in sync with no items anywhere
// - add one item on first client, copy to server
// - add a different item on second client, one-way-from-server
// - two-way sync with first client
// => one item on first client, two on second
// - delete on first client, sync that to second client
//   via two-way sync + one-way-from-server
// => one item left on second client (the one inserted locally)
void SyncTests::doOneWayFromRemote(SyncMode oneWayFromRemote) {
    // no items anywhere
    CT_ASSERT_NO_THROW(deleteAll());
    CT_ASSERT_NO_THROW(accessClientB->refreshClient());

    // check that everything is empty, also resets change tracking
    // in second sources of each client
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }

    // add one item on first client, copy to server, and check change tracking via second source
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->insertManyItems(it->second->createSourceA, 200, 1));
    }
    doSync(__FILE__, __LINE__,
           "send",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 1,0,0, true, SYNC_TWO_WAY)));
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }

    // add a different item on second client, one-way-from-server
    // => one item added locally, none sent to server
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->insertManyItems(it->second->createSourceA, 2, 1));

        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }
    accessClientB->doSync(__FILE__, __LINE__,
                          "recv",
                          SyncOptions(oneWayFromRemote,
                                      CheckSyncReport(1,0,0, 0,0,0, true, SYNC_ONE_WAY_FROM_REMOTE)));
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 2, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }

    // two-way sync with first client for verification
    // => no changes
    doSync(__FILE__, __LINE__,
           "check",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,0,0, true, SYNC_TWO_WAY)));
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }

    // delete items on clientA, sync to server
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->deleteAll(it->second->createSourceA));

        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }
    doSync(__FILE__, __LINE__,
           "delete",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,0,1, true, SYNC_TWO_WAY)));
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }

    // sync the same change to second client
    // => one item left (the one inserted locally)
    accessClientB->doSync(__FILE__, __LINE__,
                          "delete",
                          SyncOptions(oneWayFromRemote,
                                      CheckSyncReport(0,0,1, 0,0,0, true, SYNC_ONE_WAY_FROM_REMOTE)));
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }
}

// one-way-from-remote test with one-way-from-client/server, depending
// on role of remote side
void SyncTests::testOneWayFromServer()
{
    CT_ASSERT_NO_THROW(doOneWayFromRemote(OneWayFromPeerMode()));
}

void SyncTests::testOneWayFromRemote()
{
    CT_ASSERT_NO_THROW(doOneWayFromRemote(SYNC_ONE_WAY_FROM_REMOTE));
}

// tests one-way sync from local side:
// - get both clients and server in sync with no items anywhere
// - add one item on first client, copy to server
// - add a different item on second client, one-way-from-client
// - two-way sync with first client
// => two items on first client, one on second
// - delete on second client, sync that to first client
//   via one-way-from-client, two-way
// => one item left on first client (the one inserted locally)
void SyncTests::doOneWayFromLocal(SyncMode oneWayFromLocal) {
    // no items anywhere
    CT_ASSERT_NO_THROW(deleteAll());
    CT_ASSERT_NO_THROW(accessClientB->deleteAll());

    // check that everything is empty, also resets change tracking
    // in second sources of each client
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }

    // add one item on first client, copy to server, and check change tracking via second source
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->insertManyItems(it->second->createSourceA, 1, 1));
    }
    doSync(__FILE__, __LINE__,
           "send",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 1,0,0, true, SYNC_TWO_WAY)));
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }

    // add a different item on second client, one-way-from-client
    // => no item added locally, one sent to server
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->insertManyItems(it->second->createSourceA, 2, 1));

        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }
    accessClientB->doSync(__FILE__, __LINE__,
                          "send",
                          SyncOptions(oneWayFromLocal,
                                      CheckSyncReport(0,0,0, 1,0,0, true, SYNC_ONE_WAY_FROM_LOCAL)));
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }

    // two-way sync with client A for verification
    // => receive one item
    doSync(__FILE__, __LINE__,
           "check",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(1,0,0, 0,0,0, true, SYNC_TWO_WAY)));
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 2, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }

    // delete items on client B, sync to server
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->deleteAll(it->second->createSourceA));

        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }
    accessClientB->doSync(__FILE__, __LINE__,
                          "delete",
                          SyncOptions(oneWayFromLocal,
                                      CheckSyncReport(0,0,0, 0,0,1, true, SYNC_ONE_WAY_FROM_LOCAL)));
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }

    // sync the same change to client A
    // => one item left (the one inserted locally)
    doSync(__FILE__, __LINE__,
           "delete",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,1, 0,0,0, true, SYNC_TWO_WAY)));
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countNewItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 1, countDeletedItems(source.get()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countUpdatedItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }
}

// do a two-way sync without additional checks,
// may or may not actually be done in two-way mode
void SyncTests::testTwoWaySync()
{
    doSync(__FILE__, __LINE__, SyncOptions(SYNC_TWO_WAY));
}

void SyncTests::testSlowSync()
{
    doSync(__FILE__, __LINE__,
           SyncOptions(SYNC_SLOW,
                       CheckSyncReport(-1,-1,-1, -1,-1,-1, true, SYNC_SLOW)));
}

// one-way-from-local test with one-way-from-client/server, depending
// on role of local side
void SyncTests::testOneWayFromClient()
{
    CT_ASSERT_NO_THROW(doOneWayFromLocal(OneWayFromLocalMode()));
}

// do a slow sync without additional checks
void SyncTests::testOneWayFromLocal()
{
    CT_ASSERT_NO_THROW(doOneWayFromLocal(SYNC_ONE_WAY_FROM_LOCAL));
}

// get engine ready, then use it to convert our test items
// to and from the internal field list
void SyncTests::testConversion() {
    bool success = false;
    auto doConversionCallback = [this, &success] (SyncContext &syncClient,
                                                  SyncOptions &options) {
        success = false;

        for (source_it it = sources.begin(); it != sources.end(); ++it) {
            const ClientTest::Config *config = &it->second->config;
            TestingSyncSource *source = static_cast<TestingSyncSource *>(syncClient.findSource(config->m_sourceName));
            CT_ASSERT(source);

            std::string type = source->getNativeDatatypeName();
            if (type.empty()) {
                continue;
            }

            std::list<std::string> items;
            std::string testcases;
            ClientTest::getItems(config->m_testcases, items, testcases);
            std::string converted = getCurrentTest();
            converted += ".converted.";
            converted += config->m_sourceName;
            converted += ".dat";
            simplifyFilename(converted);
            std::ofstream out(converted.c_str());
            for (const string &item: items) {
                string convertedItem = item;
                if(!sysync::DataConversion(syncClient.getSession().get(),
                                           type.c_str(),
                                           type.c_str(),
                                           convertedItem)) {
                    SE_LOG_ERROR(NULL, "failed parsing as %s:\n%s",
                                 type.c_str(),
                                 item.c_str());
                } else {
                    out << convertedItem << "\n";
                }
            }
            out.close();

            // The test used peer-specific test cases, but the actual
            // result does not depend on the peer because we haven't
            // received the peer's DevInf at the point where we
            // import/export the test cases (=> don't apply peer-specific
            // synccompare workarounds).
            //
            // Due to the lack of DevInf, properties and parameters which
            // need to be enabled via DevInf get lost (= filter them out).
            ScopedEnvChange env("CLIENT_TEST_SERVER", "");
            ScopedEnvChange envParams("CLIENT_TEST_STRIP_PARAMETERS", "X-EVOLUTION-UI-SLOT");
            CT_ASSERT(config->m_compare(client, testcases, converted));
        }

        // abort sync after completing the test successfully (no exception so far!)
        success = true;
        return true;
    };
    doSync(__FILE__, __LINE__,
           SyncOptions(SYNC_TWO_WAY, CheckSyncReport(-1,-1,-1, -1,-1,-1, false))
           .setStartCallback(doConversionCallback));
    CT_ASSERT(success);
}

// imports test data, transmits it from client A to the server to
// client B and then compares which of the data has been transmitted
void SyncTests::testItems() {
    // clean server and first test database
    CT_ASSERT_NO_THROW(deleteAll());

    // import data
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->testImport());
    }

    // transfer from client A to server to client B
    doSync(__FILE__, __LINE__, "send", SyncOptions(SYNC_TWO_WAY).setWBXML(true));
    CT_ASSERT_NO_THROW(accessClientB->refreshClient(SyncOptions().setWBXML(true)));

    CT_ASSERT_NO_THROW(compareDatabases());
}

// creates several items, transmits them back and forth and
// then compares which of them have been preserved
void SyncTests::testItemsXML() {
    // clean server and first test database
    CT_ASSERT_NO_THROW(deleteAll());

    // import data
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->testImport());
    }

    // transfer from client A to server to client B using the non-default XML format
    doSync(__FILE__, __LINE__, "send", SyncOptions(SYNC_TWO_WAY).setWBXML(false));
    CT_ASSERT_NO_THROW(accessClientB->refreshClient(SyncOptions().setWBXML(false)));

    CT_ASSERT_NO_THROW(compareDatabases());
}

// imports test data, transmits it from client A to the server to
// client B, update on B and transfers back to the server,
// then compares against reference data that has the same changes
// applied on A
void SyncTests::testExtensions() {
    // clean server and first test database
    CT_ASSERT_NO_THROW(deleteAll());

    // import data and create reference data
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->testImport());

        string refDir = getCurrentTest() + "." + it->second->config.m_sourceName + ".ref.dat";
        simplifyFilename(refDir);
        rm_r(refDir);
        mkdir_p(refDir);

        TestingSyncSourcePtr source;
        int counter = 0;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
        for (const string &luid: source->getAllItems()) {
            string item;
            source->readItemRaw(luid, item);
            CT_ASSERT_NO_THROW(it->second->config.m_update(item));
            ofstream out(StringPrintf("%s/%d", refDir.c_str(), counter).c_str());
            out.write(item.c_str(), item.size());
            counter++;
        }
        CT_ASSERT_NO_THROW(source.reset());
    }

    // transfer from client A to server to client B
    doSync(__FILE__, __LINE__, "send", SyncOptions(SYNC_TWO_WAY));
    CT_ASSERT_NO_THROW(accessClientB->refreshClient(SyncOptions()));

    // update on client B
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->updateData(it->second->createSourceB));
    }

    // send back
    accessClientB->doSync(__FILE__, __LINE__, "update", SyncOptions(SYNC_TWO_WAY));
    doSync(__FILE__, __LINE__, "patch", SyncOptions(SYNC_TWO_WAY));

    bool equal = true;
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        string refDir = getCurrentTest() + "." + it->second->config.m_sourceName + ".ref.dat";
        simplifyFilename(refDir);
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
        // Compare data in source A against reference data *without*
        // telling synccompare to ignore known data loss for the
        // server. CLIENT_TEST_SERVER is relevant for finding the
        // right source config and thus setting it must come after the
        // createSourceB() call.
        ScopedEnvChange env("CLIENT_TEST_SERVER", "");
        ScopedEnvChange envParams("CLIENT_TEST_STRIP_PARAMETERS", "X-EVOLUTION-UI-SLOT"); 
        // X-EVOLUTION-FILE-AS is not preserved correctly with EDS due
        // to setting it on incoming items. It is not always set as already stored
        // locally, so when writing back, the new, artificially generated value overwrites
        // the one chosen by the user. Not ideal, but the alternative (not setting it on
        // incoming items) is worse.
        ScopedEnvChange envProps("CLIENT_TEST_STRIP_PROPERTIES", "(PHOTO|FN|X-EVOLUTION-FILE-AS)");
        if (!it->second->compareDatabases(refDir.c_str(), *source, false)) {
            equal = false;
        }
    }
    CT_ASSERT(equal);
}

// tests the following sequence of events:
// - both clients in sync with server
// - client 1 adds item
// - client 1 updates the same item
// - client 2 gets item: the client should be asked to add the item
//
// However it has been observed that sometimes the item was sent as "update"
// for a non-existant local item. This is a server bug, the client does not
// have to handle that. See
// http://forge.objectweb.org/tracker/?func=detail&atid=100096&aid=305018&group_id=96
void SyncTests::testAddUpdate() {
    // clean server and both test databases
    CT_ASSERT_NO_THROW(deleteAll());
    accessClientB->refreshClient();

    // add item
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->insert(it->second->createSourceA, it->second->config.m_insertItem, false));
    }
    doSync(__FILE__, __LINE__,
           "add",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 1,0,0, true, SYNC_TWO_WAY)));

    // update it
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->update(it->second->createSourceB, it->second->config.m_updateItem));
    }
    doSync(__FILE__, __LINE__,
           "update",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,1,0, true, SYNC_TWO_WAY)));

    // now download the updated item into the second client
    accessClientB->doSync(__FILE__, __LINE__,
                          "recv",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(1,0,0, 0,0,0, true, SYNC_TWO_WAY)));

    // compare the two databases
    CT_ASSERT_NO_THROW(compareDatabases());
}

// test copying with maxMsg and no large object support
void SyncTests::testMaxMsg()
{
    CT_ASSERT_NO_THROW(doVarSizes(true, false));
}

// test copying with maxMsg and large object support
void SyncTests::testLargeObject()
{
    CT_ASSERT_NO_THROW(doVarSizes(true, true));
}

//
// stress tests: execute some of the normal operations,
// but with large number of artificially generated items
//

// two-way sync with clean client/server,
// followed by slow sync and comparison
// via second client
void SyncTests::testManyItems() {
    // clean server and client A
    CT_ASSERT_NO_THROW(deleteAll());

    // import artificial data: make them large to generate some
    // real traffic and test buffer handling
    int num_items = defNumItems();
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->insertManyItems(it->second->createSourceA, 0, num_items, 2000));
    }

    // send data to server
    doSync(__FILE__, __LINE__,
           "send",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, num_items,0,0, true, SYNC_TWO_WAY),
                       SyncOptions::DEFAULT_MAX_MSG_SIZE,
                       SyncOptions::DEFAULT_MAX_OBJ_SIZE, 
                       true));

    // ensure that client has the same data, ignoring data conversion
    // issues (those are covered by testItems())
    CT_ASSERT_NO_THROW(refreshClient());

    // also copy to second client
    accessClientB->refreshClient();

    // slow sync now should not change anything
    doSync(__FILE__, __LINE__,
           "twinning",
           SyncOptions(SYNC_SLOW,
                       CheckSyncReport(-1,-1,-1, -1,-1,-1, true, SYNC_SLOW),
                       SyncOptions::DEFAULT_MAX_MSG_SIZE,
                       SyncOptions::DEFAULT_MAX_OBJ_SIZE, 
                       true));

    // compare
    CT_ASSERT_NO_THROW(compareDatabases());
}

/**
 * Tell server to delete plenty of items.
 */
void SyncTests::testManyDeletes() {
    // clean server and client A
    CT_ASSERT_NO_THROW(deleteAll());

    // import artificial data: make them small, we just want
    // many of them
    int num_items = defNumItems();
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->insertManyItems(it->second->createSourceA, 0, num_items, 100));
    }

    // send data to server
    doSync(__FILE__, __LINE__,
           "send",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, num_items,0,0, true, SYNC_TWO_WAY),
                       64 * 1024, 64 * 1024, true));

    // ensure that client has the same data, ignoring data conversion
    // issues (those are covered by testItems())
    CT_ASSERT_NO_THROW(refreshClient());

    // also copy to second client
    accessClientB->refreshClient();

    // slow sync now should not change anything
    doSync(__FILE__, __LINE__,
           "twinning",
           SyncOptions(SYNC_SLOW,
                       CheckSyncReport(-1,-1,-1, -1,-1,-1, true, SYNC_SLOW),
                       64 * 1024, 64 * 1024, true));

    // compare
    CT_ASSERT_NO_THROW(compareDatabases());

    // delete everything locally
    CT_ASSERT_NO_THROW(allSourcesDeleteAll());
    doSync(__FILE__, __LINE__,
           "delete-server",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,0,num_items, true, SYNC_TWO_WAY),
                       10 * 1024));

    // Reporting locally deleted items depends on sync mode
    // recognition, see SyncContext.cpp.
    const char* checkSyncModeStr = getenv("CLIENT_TEST_NOCHECK_SYNCMODE");    

    // update second client
    accessClientB->doSync(__FILE__, __LINE__,
                          "delete-client",
                          SyncOptions(RefreshFromPeerMode(),
                                      checkSyncModeStr ? CheckSyncReport() :
                                      CheckSyncReport(0,0,num_items, 0,0,0, true, SYNC_REFRESH_FROM_REMOTE),
                                      10 * 1024));
}

/**
 * - get client A, server, client B in sync with one item
 * - force slow sync in A: must not duplicate items, but may update it locally
 * - refresh client B (in case that the item was updated)
 * - delete item in B and server via two-way sync
 * - refresh-from-server in B to check that item is gone
 * - two-way in A: must delete the item
 */
void SyncTests::testSlowSyncSemantic()
{
    // set up one item everywhere
    CT_ASSERT_NO_THROW(doCopy());

    // slow in A
    doSync(__FILE__, __LINE__,
           "slow",
           SyncOptions(SYNC_SLOW,
                       CheckSyncReport(0,-1,0, -1,-1,0, true, SYNC_SLOW)));

    // refresh B, delete item
    accessClientB->doSync(__FILE__, __LINE__,
                          "refresh",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(0,-1,0, 0,0,0, true, SYNC_TWO_WAY)));
    CT_ASSERT_NO_THROW(accessClientB->allSourcesDeleteAll());
    accessClientB->doSync(__FILE__, __LINE__,
                          "delete",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(0,0,0, 0,0,1, true, SYNC_TWO_WAY)));
    accessClientB->doSync(__FILE__, __LINE__,
                          "check",
                          SyncOptions(RefreshFromPeerMode(),
                                      CheckSyncReport(0,0,0, 0,0,0, true, SYNC_REFRESH_FROM_REMOTE)));

    // now the item should also be deleted on A
    doSync(__FILE__, __LINE__,
           "delete",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,1, 0,0,0, true, SYNC_TWO_WAY)));
}

/**
 * check that refresh-from-server works correctly:
 * - create the same item on A, server, B via testCopy()
 * - refresh B (one item deleted, one created)
 * - delete item on A and server
 * - refresh B (one item deleted)
 */
void SyncTests::testComplexRefreshFromServerSemantic()
{
    CT_ASSERT_NO_THROW(testCopy());

    // Reporting locally deleted items depends on sync mode
    // recognition, see SyncContext.cpp.
    const char* checkSyncModeStr = getenv("CLIENT_TEST_NOCHECK_SYNCMODE");    

    // check refresh with one item on server
    const char *value = getenv ("CLIENT_TEST_NOREFRESH");
    // If refresh_from_server or refresh_from_client (depending on this is a
    // server or client) is not supported, we can still test via slow sync.
    if (value) {
        accessClientB->refreshClient();
    } else {
        accessClientB->doSync(__FILE__, __LINE__,
                              "refresh-one",
                              SyncOptions(RefreshFromPeerMode(),
                                          checkSyncModeStr ? CheckSyncReport() :
                                          CheckSyncReport(1,0,1, 0,0,0, true, SYNC_REFRESH_FROM_REMOTE)));
    }

    // delete that item via A, check again
    CT_ASSERT_NO_THROW(allSourcesDeleteAll());
    doSync(__FILE__, __LINE__,
           "delete-item",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,0,1, true, SYNC_TWO_WAY)));
    if (value) {
        accessClientB->refreshClient();
    } else {
        accessClientB->doSync(__FILE__, __LINE__,
                              "refresh-none",
                              SyncOptions(RefreshFromPeerMode(),
                                          checkSyncModeStr ? CheckSyncReport() :
                                          CheckSyncReport(0,0,1, 0,0,0, true, SYNC_REFRESH_FROM_REMOTE)));
    }
}

/**
 * - create the same item on A, server, B via testCopy()
 * - delete on both sides
 * - sync A
 * - sync B
 *
 * Must not fail, even though the Synthesis engine will ask the backends
 * for deletion of an already deleted item.
 */
void SyncTests::testDeleteBothSides()
{
    CT_ASSERT_NO_THROW(testCopy());

    CT_ASSERT_NO_THROW(allSourcesDeleteAll());
    CT_ASSERT_NO_THROW(accessClientB->allSourcesDeleteAll());

    doSync(__FILE__, __LINE__,
           "delete-item-A",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,0,1, true, SYNC_TWO_WAY)));
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }


    // it is undefined whether the item is meant to be reported as deleted again here:
    // a SyncML client test will mark it as deleted, local sync as server won't
    accessClientB->doSync(__FILE__, __LINE__,
                          "delete-item-B",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(0,0,0, 0,0,-1, true, SYNC_TWO_WAY)));
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        if (it->second->config.m_createSourceB) {
            TestingSyncSourcePtr source;
            SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceB()));
            SOURCE_ASSERT_EQUAL(source.get(), 0, countItems(source.get()));
            CT_ASSERT_NO_THROW(source.reset());
        }
    }
}

/**
 * - clean A, server, B
 * - create an item on A
 * - sync A
 * - create a modified version of the item on B
 * - sync B
 *
 * Depends on UID and LAST-MODIFIED in item data, i.e., iCalendar 2.0.
 * Uses the normal "insertItem" test case. Only works for a single source.
 *
 * The server must not duplicate the item *and* preserve the modified
 * properties.
 *
 * Temporary: because conflict resolution is server-dependent, such a strict
 * test fails. For example, with SyncEvolution 1.2 as server, DESCRIPTION and
 * LOCATION end up being concatenated (merge=lines mode). The test now avoids
 * using different data, with the expected outcome that only one item
 * is present at the end and no unnecessary data transfers happen (only true
 * for SyncEvolution server).
 *
 * A similar situation occurs on the client side, but it is harder to
 * trigger: the updated item must be added to the client's database
 * after it has reported its changes. Because if it happens earlier,
 * it would send an Add to the server and the server would have to
 * resolve the add<->add conflict, as in this test here.
 */

// using updated item data makes the test harder to pass:
// server must use exactly the right item, which currently
// is not the case for SyncEvolution
bool addBothSidesUsesUpdateItem = true;

// SyncEvolution passes with addBothSidesUsesUpdateItem == true
// if we avoid changes to those properties in the iCalendar test
// set which currently use merge=lines.
bool addBothSidesNoMergeLines=true;

// if true, relax expectations for updates from server:
// may or may not send one
bool addBothSidesMayUpdate = false;

// if true, then accept that the Synthesis server mode counts
// Add commands as "added items" even if they are turned into updates
bool addBothSidesAddStatsBroken = false;

// if true, then the peer is a SyncML server which does not
// support UID/RECURRENCE-ID and thus doesn't detect
// duplicates itself; the client needs to do that
bool addBothSidesServerIsDumb = getenv("CLIENT_TEST_ADD_BOTH_SIDES_SERVER_IS_DUMB") != nullptr;

static void testAddBothSidesFixUpdateItem(std::string &updateItem)
{
    if (addBothSidesNoMergeLines) {
        // VEVENT
        boost::replace_all(updateItem, "LOCATION:big meeting room", "LOCATION:my office");
        boost::replace_all(updateItem, "DESCRIPTION:nice to see you", "DESCRIPTION:let's talk<<REVISION>>");
        // VJOURNAL
        boost::replace_all(updateItem, "DESCRIPTION:Summary\\nBody text", "DESCRIPTION:Summary Modified\\nBody text");
        // VTODO
        boost::replace_all(updateItem, "DESCRIPTION:to be done", "DESCRIPTION:to be done<<REVISION>>");
    }
}

void SyncTests::testAddBothSides()
{
    CT_ASSERT_NO_THROW(deleteAll());
    accessClientB->deleteAll();

    std::string insertItem = sources[0].second->config.m_insertItem;
    std::string updateItem = sources[0].second->config.m_updateItem;
    testAddBothSidesFixUpdateItem(updateItem);

    CT_ASSERT_NO_THROW(sources[0].second->insert(sources[0].second->createSourceA,
                                                      insertItem));

    doSync(__FILE__, __LINE__,
           "send-old",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 1,0,0, true, SYNC_TWO_WAY)));

    // insert updated item data on B
    std::string data;
    CT_ASSERT_NO_THROW(accessClientB->sources[0].second->insert(accessClientB->sources[0].second->createSourceA,
                                                                addBothSidesUsesUpdateItem ?
                                                                updateItem:
                                                                insertItem,
                                                                false,
                                                                &data));

    // As far as the client knows, it is adding an item;
    // server not expected to send back an update (our data was more recent
    // and completely overwrites the server's data).
    // When acting as server, we do the duplicate detection and thus know
    // more about the actual outcome.
    accessClientB->doSync(__FILE__, __LINE__,
                          "send-update",
                          SyncOptions(SYNC_TWO_WAY,
                                      isServerMode() ?
                                      CheckSyncReport(addBothSidesAddStatsBroken ? -1 : 0,0,0,

                                                      0,
                                                      addBothSidesMayUpdate ? -1 :
                                                      addBothSidesUsesUpdateItem ? 1 : 0,
                                                      0,

                                                      true, SYNC_TWO_WAY) :
                                      addBothSidesServerIsDumb ?
                                      CheckSyncReport(addBothSidesServerIsDumb ? 1 : 0,
                                                      addBothSidesMayUpdate ? -1 : 0,
                                                      0,

                                                      // client got one redundant item from
                                                      // server, had to receive it, match against
                                                      // its own copy, then tell the server to
                                                      // update one copy and delete the other;
                                                      // no update necessary on server because
                                                      // it already had the latest copy
                                                      1,0,1, true, SYNC_TWO_WAY).setRestarts(1) :
                                      CheckSyncReport(0,
                                                      addBothSidesMayUpdate ? -1 : 0,
                                                      0,

                                                      // client doesn't know that the add
                                                      // was an update, in contrast to server
                                                      1,0,0, true, SYNC_TWO_WAY)));

    // update sent to client A
    doSync(__FILE__, __LINE__,
           "update",
           SyncOptions(SYNC_TWO_WAY,
                       (!isServerMode() && addBothSidesServerIsDumb) ?
                       // server had to be told to update old item
                       // and delete redundant one, which is what it now
                       // also tells us here
                       CheckSyncReport(1,0,1,
                                       0,0,0, true, SYNC_TWO_WAY) :
                       CheckSyncReport(0,
                                       addBothSidesMayUpdate ? -1 :
                                       addBothSidesUsesUpdateItem ? 1 : 0,
                                       0,

                                       0,0,0, true, SYNC_TWO_WAY)));

    // nothing necessary for client B
    accessClientB->doSync(__FILE__, __LINE__,
                          "nop",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(0,0,0, 0,0,0, true, SYNC_TWO_WAY)));

    // now compare client A against reference data
    TestingSyncSourcePtr copy;
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(sources[0].second->createSourceB()));
    sources[0].second->compareDatabases(copy.get(), &data, nullptr);
    CT_ASSERT_NO_THROW(copy.reset());
}

/**
 * compared to testAddBothSides the age of the items is reversed now;
 * a server which always copies the client's data passes testAddBothSides
 * but fails here
 */
void SyncTests::testAddBothSidesRefresh()
{
    CT_ASSERT_NO_THROW(deleteAll());
    accessClientB->deleteAll();

    std::string insertItem = sources[0].second->config.m_insertItem;
    std::string updateItem = sources[0].second->config.m_updateItem;
    testAddBothSidesFixUpdateItem(updateItem);

    // insert initial item data on B
    CT_ASSERT_NO_THROW(accessClientB->sources[0].second->insert(accessClientB->sources[0].second->createSourceA,
                                                                     insertItem));

    // sleep one second to ensure that it's mangled LAST-MODIFIED is older than
    // the one from the next item, inserted on A
    sleep(1);

    // more recent data sent to server first
    std::string data;
    CT_ASSERT_NO_THROW(sources[0].second->insert(sources[0].second->createSourceA,
                                                      addBothSidesUsesUpdateItem ?
                                                      updateItem :
                                                      insertItem,
                                                      false,
                                                      &data));
    doSync(__FILE__, __LINE__,
           "send-new",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 1,0,0, true, SYNC_TWO_WAY)));

    // As far as the client knows, it is adding an item;
    // server expected to send back an update (client's data was out-dated);
    // When acting as server, we do the duplicate detection and thus
    // know more about the actual outcome.
    accessClientB->doSync(__FILE__, __LINE__,
                          "send-old",
                          SyncOptions(SYNC_TWO_WAY,
                                      isServerMode() ?
                                      CheckSyncReport(addBothSidesAddStatsBroken ? -1 : 0,
                                                      addBothSidesMayUpdate ? -1 :
                                                      addBothSidesUsesUpdateItem ? 1 : 0,
                                                      0,

                                                      0,
                                                      addBothSidesMayUpdate ? -1 : 0,
                                                      0,

                                                      true, SYNC_TWO_WAY) :
                                      // When the server is dumb, it
                                      // will just accept the added
                                      // item and send us an <Add>
                                      // with an item that has the
                                      // same UID as the one it just
                                      // received. The client then
                                      // must start a second sync and
                                      // fix the server by sending an update
                                      // (of the old version) and a delete (of the
                                      // new one)
                                      addBothSidesServerIsDumb ?
                                      CheckSyncReport(1,0,0,
                                                      1,1,1, true, SYNC_TWO_WAY).setRestarts(1) :
                                      CheckSyncReport(0,
                                                      addBothSidesMayUpdate ? -1 :
                                                      addBothSidesUsesUpdateItem ? 1 : 0,
                                                      0,

                                                      // client doesn't know that add was
                                                      // an update
                                                      1,0,0, true, SYNC_TWO_WAY)));

    // potentially send update to A
    doSync(__FILE__, __LINE__,
           "nopA",
           SyncOptions(SYNC_TWO_WAY,
                       (!isServerMode() && addBothSidesServerIsDumb) ?
                       // receives extra changes because dumb server had to be fixed
                       CheckSyncReport(1,0,1, 0,0,0, true, SYNC_TWO_WAY) :
                       CheckSyncReport(0,addBothSidesMayUpdate ? -1 : 0,0, 0,0,0, true, SYNC_TWO_WAY)));

    // nothing necessary for client B (already synchronized completely above in one sync)
    accessClientB->doSync(__FILE__, __LINE__,
                          "nopB",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(0,0,0, 0,0,0, true, SYNC_TWO_WAY)));

    // now compare client A against reference data
    TestingSyncSourcePtr copy;
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(sources[0].second->createSourceB()));
    sources[0].second->compareDatabases(copy.get(), &data, nullptr);
    CT_ASSERT_NO_THROW(copy.reset());
}

/**
 * - adds parent on client A
 * - syncs A
 * - adds unrelated item via client B (necessary to trigger corner cases in
 *   change tracking, see BMC #22329)
 * - syncs B
 * - adds child on client A
 * - syncs A and B
 * - compares
 */
void SyncTests::testLinkedItemsParentChild()
{
    // clean server, client A and client B
    CT_ASSERT_NO_THROW(deleteAll());
    accessClientB->refreshClient();

    // create and copy parent item
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT(!it->second->config.m_linkedItems.empty());
        CT_ASSERT(it->second->config.m_linkedItems[0].size() >= 2);
        TestingSyncSourcePtr source;
        CT_ASSERT_NO_THROW(it->second->insert(it->second->createSourceA,
                                              it->second->config.m_linkedItems[0][0],
                                              false));
    }
    doSync(__FILE__, __LINE__,
           "send-parent",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 1,0,0, true, SYNC_TWO_WAY)));

    // create independent item, refresh client B and server
    for (source_it it = accessClientB->sources.begin(); it != accessClientB->sources.end(); ++it) {
        CT_ASSERT_NO_THROW(it->second->insert(it->second->createSourceA,
                                              it->second->config.m_insertItem,
                                              false));
    }
    accessClientB->doSync(__FILE__, __LINE__,
                          "recv-parent",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(1,0,0, 1,0,0, true, SYNC_TWO_WAY)));

    // add child on client A
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT(!it->second->config.m_linkedItems.empty());
        CT_ASSERT(it->second->config.m_linkedItems[0].size() >= 2);
        TestingSyncSourcePtr source;
        CT_ASSERT_NO_THROW(it->second->insert(it->second->createSourceA,
                                              it->second->config.m_linkedItems[0][1],
                                              false));
    }
    // parent may or may not be considered updated
    doSync(__FILE__, __LINE__,
           "send-child",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(1,0,0, 1,-1,0, true, SYNC_TWO_WAY)));
    // parent may or may not be considered updated here
    accessClientB->doSync(__FILE__, __LINE__,
                          "recv-child",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(1,-1,0, 0,0,0, true, SYNC_TWO_WAY)));

    // final comparison
    CT_ASSERT_NO_THROW(compareDatabases());
}

/**
 * - adds child on client A
 * - syncs A
 * - syncs B
 * - compare
 */
void SyncTests::testLinkedItemsChild()
{
    // clean server, client A and client B
    CT_ASSERT_NO_THROW(deleteAll());
    accessClientB->refreshClient();

    // create and copy child item
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT(!it->second->config.m_linkedItems.empty());
        CT_ASSERT(it->second->config.m_linkedItems[0].size() >= 2);
        TestingSyncSourcePtr source;
        CT_ASSERT_NO_THROW(it->second->insert(it->second->createSourceA,
                                              it->second->config.m_linkedItems[0][1],
                                              false));
    }
    doSync(__FILE__, __LINE__,
           "send",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 1,0,0, true, SYNC_TWO_WAY)));
    accessClientB->doSync(__FILE__, __LINE__,
                          "recv",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(1,0,0, 0,0,0, true, SYNC_TWO_WAY)));

    // final comparison
    CT_ASSERT_NO_THROW(compareDatabases());
}


/**
 * - adds child on client A
 * - syncs A and B
 * - adds parent on client A
 * - syncs A and B
 * - compares
 */
void SyncTests::testLinkedItemsChildParent()
{
    // clean server, client A and client B
    CT_ASSERT_NO_THROW(deleteAll());
    accessClientB->refreshClient();

    // create and copy child item
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT(!it->second->config.m_linkedItems[0].empty());
        CT_ASSERT(it->second->config.m_linkedItems[0].size() >= 2);
        TestingSyncSourcePtr source;
        CT_ASSERT_NO_THROW(it->second->insert(it->second->createSourceA,
                                              it->second->config.m_linkedItems[0][1],
                                              false));
    }
    doSync(__FILE__, __LINE__,
           "send-child",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 1,0,0, true, SYNC_TWO_WAY)));
    accessClientB->doSync(__FILE__, __LINE__,
                          "recv-child",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(1,0,0, 0,0,0, true, SYNC_TWO_WAY)));

    // add parent on client A
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        CT_ASSERT(!it->second->config.m_linkedItems.empty());
        CT_ASSERT(it->second->config.m_linkedItems[0].size() >= 2);
        TestingSyncSourcePtr source;
        // relaxed change checks because child event is also modified
        CT_ASSERT_NO_THROW(it->second->insert(it->second->createSourceA,
                                              it->second->config.m_linkedItems[0][0],
                                              true));
    }
    // child may or may not be considered updated
    doSync(__FILE__, __LINE__,
           "send-parent",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 1,-1,0, true, SYNC_TWO_WAY)));
    // child may or may not be considered updated here
    accessClientB->doSync(__FILE__, __LINE__,
                          "recv-parent",
                          SyncOptions(SYNC_TWO_WAY,
                                      CheckSyncReport(1,-1,0, 0,0,0, true, SYNC_TWO_WAY)));

    // final comparison
    CT_ASSERT_NO_THROW(compareDatabases());
}

/**
 * implements testMaxMsg(), testLargeObject(), testLargeObjectEncoded()
 * using a sequence of items with varying sizes
 */
void SyncTests::doVarSizes(bool withMaxMsgSize,
                           bool withLargeObject) {
    int maxMsgSize = 8 * 1024;
    const char* maxItemSize = getenv("CLIENT_TEST_MAX_ITEMSIZE");
    int tmpSize = maxItemSize ? atoi(maxItemSize) : 0;
    if(tmpSize > 0) 
        maxMsgSize = tmpSize;

    // clean server and client A
    CT_ASSERT_NO_THROW(deleteAll());

    // insert items, doubling their size, then restart with small size
    for (source_it it = sources.begin(); it != sources.end(); ++it) {
        int item = 1;
        restoreStorage(it->second->config, client);
        TestingSyncSourcePtr source;
        SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceA()));
        for (int i = 0; i < 2; i++ ) {
            int size = 1;
            while (size < 2 * maxMsgSize) {
                CT_ASSERT_NO_THROW(it->second->insertManyItems(source.get(), item, 1, it->second->config.m_templateItem.size() + 10 + size));
                size *= 2;
                item++;
            }
        }
        backupStorage(it->second->config, client);
    }

    // transfer to server
    doSync(__FILE__, __LINE__,
           "send",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, -1,0,0, true, SYNC_TWO_WAY), // number of items sent to server depends on source
                       withMaxMsgSize ? SyncOptions::DEFAULT_MAX_MSG_SIZE: 0,
                       withMaxMsgSize ? SyncOptions::DEFAULT_MAX_OBJ_SIZE : 0,
                       withLargeObject));

    // copy to second client
    const char *value = getenv ("CLIENT_TEST_NOREFRESH");
    // If refresh_from_server or refresh_from_client (depending on this is a
    // server or client) is not supported, we can still test via slow sync.
    if (value) {
        accessClientB->refreshClient();
    } else {
        accessClientB->doSync(__FILE__, __LINE__,
                "recv",
                SyncOptions(RefreshFromPeerMode(),
                    CheckSyncReport(-1,0,-1, 0,0,0, true, SYNC_REFRESH_FROM_REMOTE), // number of items received from server depends on source
                    withLargeObject ? maxMsgSize : withMaxMsgSize ? maxMsgSize * 100 /* large enough so that server can sent the largest item */ : 0,
                    withMaxMsgSize ? maxMsgSize * 100 : 0,
                    withLargeObject));
    }
    // compare
    CT_ASSERT_NO_THROW(compareDatabases());
}

/**
 * Send message to server, then pretend that we timed out at exactly
 * one specific message, specified via m_interruptAtMessage.  The
 * caller is expected to resend the message, without aborting the
 * session. That resend and all following message will get through
 * again.
 *
 * Each send() is counted as one message, starting at 1 for the first
 * message.
 */
class TransportResendInjector : public TransportWrapper{
private:
    int timeout;
public:
    TransportResendInjector()
         :TransportWrapper() {
             const char *s = getenv("CLIENT_TEST_RESEND_TIMEOUT");
             timeout = s ? atoi(s) : 0;
    }

    ~TransportResendInjector() {
    }

    virtual int getResendFailureThreshold() { return 0; }

    virtual void send(const char *data, size_t len)
    {
        m_messageCount++;
        if (m_interruptAtMessage >= 0 &&
                m_messageCount == m_interruptAtMessage+1) {
            m_wrappedAgent->send(data, len);
            m_status = m_wrappedAgent->wait();
            //trigger client side resend
            sleep (timeout);
            m_status = TIME_OUT;
        }
        else 
        {
            m_wrappedAgent->send(data, len);
            m_status = m_wrappedAgent->wait();
        }
    }

    virtual void getReply(const char *&data, size_t &len, std::string &contentType) {
        if (m_status == FAILED) {
            data = "";
            len = 0;
        } else {
            m_wrappedAgent->getReply(data, len, contentType);
        }
    }
};

/**
 * Stop sending at m_interruptAtMessage. The caller is forced to abort
 * the current session and will recover by retrying in another
 * session.
 *
 * Each send() increments the counter by two, so that 1 aborts before
 * the first message and 2 after it.
 */
class TransportFaultInjector : public TransportWrapper{
public:
    TransportFaultInjector()
         :TransportWrapper() {
    }

    ~TransportFaultInjector() {
    }

    virtual void send(const char *data, size_t len)
    {
        if (m_interruptAtMessage == m_messageCount) {
            SE_LOG_DEBUG(NULL, "TransportFaultInjector: interrupt before sending message #%d", m_messageCount);
        }
        m_messageCount++;
        if (m_interruptAtMessage >= 0 &&
            m_messageCount > m_interruptAtMessage) {
            throw string("TransportFaultInjector: interrupt before send");
        }
    
        m_wrappedAgent->send(data, len);

        m_status = m_wrappedAgent->wait();
        
        if (m_interruptAtMessage == m_messageCount) {
            SE_LOG_DEBUG(NULL, "TransportFaultInjector: interrupt after receiving reply #%d", m_messageCount);
        }
        m_messageCount++;
        if (m_interruptAtMessage >= 0 &&
            m_messageCount > m_interruptAtMessage) {
            m_status = FAILED;
        }
    }

    virtual void getReply(const char *&data, size_t &len, std::string &contentType) {
        if (m_status == FAILED) {
            data = "";
            len = 0;
        } else {
            m_wrappedAgent->getReply(data, len, contentType);
        }
    }
};

/**
 * Swallow data at various points:
 * - between "client sent data" and "server receives data"
 * - after "server received data" and before "server sends reply"
 * - after "server has sent reply"
 *
 * The client deals with it by resending. This is similar to
 * TransportResendInjector and the ::Resend tests, but more thorough,
 * and stresses the HTTP server more (needs to deal with "reply not
 * delivered" error).
 *
 * Each send() increments the counter by three, so that 0 aborts
 * before the first message, 1 after sending it, and 2 after receiving
 * its reply.
 *
 * Swallowing data is implemented via the proxy.py script. This is
 * necessary because the wrapped agent has no API to trigger the second
 * error scenario. The wrapped agent is told to use a specific port
 * on localhost, with the base port passing message and reply through,
 * "base + 1" intercepting the message, etc.
 *
 * Because of the use of a proxy, this cannot be used to test servers
 * where a real proxy is needed.
 */
class TransportResendProxy : public TransportWrapper {
private:
    int port;
public:
    TransportResendProxy() : TransportWrapper() {
        const char *s = getenv("CLIENT_TEST_RESEND_PROXY");
        port = s ? atoi(s) : 0;
    }

    virtual int getResendFailureThreshold() { return 2; }

    virtual void send(const char *data, size_t len)
    {
        HTTPTransportAgent *agent = dynamic_cast<HTTPTransportAgent *>(m_wrappedAgent.get());
        CT_ASSERT(agent);

        m_messageCount += 3;
        if (m_interruptAtMessage >= 0 &&
            m_interruptAtMessage < m_messageCount &&
            m_interruptAtMessage >= m_messageCount - 3) {
            int offset = m_interruptAtMessage - m_messageCount + 4;
            SE_LOG_DEBUG(NULL, "TransportResendProxy: interrupt %s",
                         offset == 1 ? "before sending message" :
                         offset == 2 ? "directly after sending message" :
                         "after receiving reply");
            agent->setProxy(StringPrintf("http://127.0.0.1:%d",
                                         offset + port));
        } else {
            agent->setProxy("");
        }
        agent->send(data, len);
        m_status = agent->wait();
    }

    virtual void getReply(const char *&data, size_t &len, std::string &contentType) {
        if (m_status == FAILED) {
            data = "";
            len = 0;
        } else {
            m_wrappedAgent->getReply(data, len, contentType);
        }
    }
};


/**
 * Emulates a user suspend just after receving response 
 * from server.
 */
class UserSuspendInjector : public TransportWrapper{
public:
    UserSuspendInjector()
         :TransportWrapper() {
    }

    ~UserSuspendInjector() {
    }

    virtual void send(const char *data, size_t len)
    {
        m_wrappedAgent->send(data, len);
        m_status = m_wrappedAgent->wait();
    }

    virtual void getReply(const char *&data, size_t &len, std::string &contentType) {
        if (m_status == FAILED) {
            data = "";
            len = 0;
        } else {
            if (m_interruptAtMessage == m_messageCount) {
                 SE_LOG_DEBUG(NULL, "UserSuspendInjector: user suspend after getting reply #%d", m_messageCount);
            }
            m_messageCount++;
            if (m_interruptAtMessage >= 0 &&
                    m_messageCount > m_interruptAtMessage) {
                m_options->m_isSuspended = SuspendFlags::getSuspendFlags().suspend();
            }
            m_wrappedAgent->getReply(data, len, contentType);
        }
    }
};

/**
 * This function covers different error scenarios that can occur
 * during real synchronization. To pass, clients must either force a
 * slow synchronization after a failed synchronization or implement
 * the error handling described in the design guide (track server's
 * status for added/updated/deleted items and resend unacknowledged
 * changes).
 *
 * The items used during these tests are synthetic. They are
 * constructed so that normally a server should be able to handle
 * twinning during a slow sync correctly.
 *
 * Errors are injected into a synchronization by wrapping the normal
 * HTTP transport agent. The wrapper enumerates messages sent between
 * client and server (i.e., one message exchange increments the
 * counter by two), starting from zero. It "cuts" the connection before
 * sending out the next message to the server respectively after the 
 * server has replied, but before returning the reply to the client.
 * The first case simulates a lost message from the client to the server
 * and the second case a lost message from the server to the client.
 *
 * The expected result is the same as in an uninterrupted sync, which
 * is done once at the beginning.
 *
 * Each test goes through the following steps:
 * - client A and B reset local data store
 * - client A creates 3 new items, remembers LUIDs
 * - refresh-from-client A sync
 * - refresh-from-client B sync
 * - client B creates 3 different items, remembers LUIDs
 * - client B syncs
 * - client A syncs => A, B, server are in sync
 * - client A modifies his items (depends on test) and
 *   sends changes to server => server has changes for B
 * - client B modifies his items (depends on test)
 * - client B syncs, transport wrapper simulates lost message n
 * - client B syncs again, resuming synchronization if possible or
 *   slow sync otherwise (responsibility of the client!)
 * - client A syncs (not tested yet: A should be sent exactly the changes made by B)
 * - test that A and B contain same items
 * - test that A contains the same items as the uninterrupted reference run
 * - repeat the steps above ranging starting with lost message 0 until no
 *   message got lost
 *
 * Set the CLIENT_TEST_INTERRUPT_AT env variable to a message number
 * >= 0 to execute one uninterrupted run and then interrupt at that
 * message. Set to -1 to just do the uninterrupted run.
 */
void SyncTests::doInterruptResume(int changes, 
                  std::shared_ptr<TransportWrapper> wrapper)
{
    int interruptAtMessage = -1;
    const char *t = getenv("CLIENT_TEST_INTERRUPT_AT");
    int requestedInterruptAt = t ? atoi(t) : -2;
    const char *s = getenv("CLIENT_TEST_INTERRUPT_SLEEP");
    int sleep_t = s ? atoi(s) : 0;
    size_t i;
    std::string refFileBase = getCurrentTest() + ".ref.";
    bool equal = true;
    bool resend = wrapper->getResendFailureThreshold() != -1;
    bool suspend = dynamic_cast <UserSuspendInjector *> (wrapper.get()) != nullptr;
    bool interrupt = dynamic_cast <TransportFaultInjector *> (wrapper.get()) != nullptr;

    // better be large enough for complete DevInf, 20000 is already a
    // bit small when running with many stores
    size_t maxMsgSize = 20000;
    size_t changedItemSize = (changes & BIG) ?
        5 * maxMsgSize / 2 : // large enough to be split over three messages
        0;

    // After running the uninterrupted sync, we remember the number
    // of sent messages. We never interrupt between sending our
    // own last message and receiving the servers last reply,
    // because the server is unable to detect that we didn't get
    // the reply. It will complete the session whereas the client
    // suspends, leading to an unexpected slow sync the next time.
    int maxMsgNum = 0;

    while (true) {
        char buffer[80];
        sprintf(buffer, "%d", interruptAtMessage);
        const char *prefix = interruptAtMessage == -1 ? "complete" : buffer;
        SyncPrefix prefixA(prefix, *this);
        SyncPrefix prefixB(prefix, *accessClientB);

        std::vector< std::list<std::string> > clientAluids;
        std::vector< std::list<std::string> > clientBluids;

        // create new items in client A and sync to server
        clientAluids.resize(sources.size());
        for (i = 0; i < sources.size(); i++) {
            sources[i].second->deleteAll(sources[i].second->createSourceA);
            clientAluids[i] =
                sources[i].second->insertManyItems(sources[i].second->createSourceA,
                                                   1, 3, 0);
        }
        doSync(__FILE__, __LINE__, "fromA", SyncOptions(RefreshFromLocalMode()));

        // init client B and add its items to server and client A
        accessClientB->doSync(__FILE__, __LINE__, "initB", SyncOptions(RefreshFromPeerMode()));
        clientBluids.resize(sources.size());
        for (i = 0; i < sources.size(); i++) {
            clientBluids[i] =
                accessClientB->sources[i].second->insertManyItems(accessClientB->sources[i].second->createSourceA,
                                                                  11, 3, 0);
        }
        accessClientB->doSync(__FILE__, __LINE__, "fromB", SyncOptions(SYNC_TWO_WAY));
        doSync(__FILE__, __LINE__, "updateA", SyncOptions(SYNC_TWO_WAY));

        // => client A, B and server in sync with a total of six items

        // make changes as requested on client A and sync to server
        for (i = 0; i < sources.size(); i++) {
            if (changes & SERVER_ADD) {
                sources[i].second->insertManyItems(sources[i].second->createSourceA,
                                                   4, 1, changedItemSize);
            }
            if (changes & SERVER_REMOVE) {
                // remove second item
                removeItem(sources[i].second->createSourceA,
                           *(++clientAluids[i].begin()));
            }
            if (changes & SERVER_UPDATE) {
                // update third item
                updateItem(sources[i].second->createSourceA,
                           sources[i].second->config,
                           *(++ ++clientAluids[i].begin()),
                           sources[i].second->createItem(3, "updated", changedItemSize).c_str());
            }
        }

        // send using the same mode as in the interrupted sync with client B
        if (changes & (SERVER_ADD|SERVER_REMOVE|SERVER_UPDATE)) {
            doSync(__FILE__, __LINE__, "changesFromA", SyncOptions(SYNC_TWO_WAY).setMaxMsgSize(maxMsgSize));
        }

        // make changes as requested on client B
        for (i = 0; i < sources.size(); i++) {
            if (changes & CLIENT_ADD) {
                accessClientB->sources[i].second->insertManyItems(accessClientB->sources[i].second->createSourceA,
                                                                  14, 1, changedItemSize);
            }
            if (changes & CLIENT_REMOVE) {
                // remove second item
                removeItem(accessClientB->sources[i].second->createSourceA,
                           *(++clientBluids[i].begin()));
            }
            if (changes & CLIENT_UPDATE) {
                // update third item
                updateItem(accessClientB->sources[i].second->createSourceA,
                           accessClientB->sources[i].second->config,
                           *(++ ++clientBluids[i].begin()),
                           accessClientB->sources[i].second->createItem(13, "updated", changedItemSize).c_str());
            }
        }

        // Now do an interrupted sync between B and server.
        // The explicit delete of the TransportAgent is suppressed
        // by overloading the delete operator.
        int wasInterrupted;
        {
            CheckSyncReport check(-1, -1, -1, -1, -1, -1, false);
            if (resend && interruptAtMessage > wrapper->getResendFailureThreshold()) {
                // resend tests must succeed, except for the first
                // message in the session, which is not resent
                check.mustSucceed = true;
            }
            SyncOptions options(SYNC_TWO_WAY, check);
            options.setTransportAgent(wrapper);
            options.setMaxMsgSize(maxMsgSize);
            // disable resending completely or shorten the resend
            // interval to speed up testing
            options.setRetryInterval(resend ? 10 : 0);
            wrapper->setInterruptAtMessage(interruptAtMessage);
            accessClientB->doSync(__FILE__, __LINE__, "changesFromB", options);
            wasInterrupted = interruptAtMessage != -1 &&
                wrapper->getMessageCount() <= interruptAtMessage;
            if (!maxMsgNum) {
                maxMsgNum = wrapper->getMessageCount();
            }
            wrapper->rewind();
        }

        if (interruptAtMessage != -1) {
            if (wasInterrupted) {
                // uninterrupted sync, done
                break;
            }

            // continue, wait until server timeout
            if(sleep_t) 
                sleep (sleep_t);

            // no need for resend tests, unless they were interrupted at the first message
            if (!resend || interruptAtMessage <= wrapper->getResendFailureThreshold()) {
                SyncReport report;
                accessClientB->doSync(__FILE__, __LINE__,
                                      "retryB",
                                      SyncOptions(SYNC_TWO_WAY,
                                                  CheckSyncReport().setMode(SYNC_TWO_WAY).setReport(&report)));
                // Suspending at first and last message doesn't need a
                // resume, everything else does. When multiple sources
                // are involved, some may suspend, some may not, so we
                // cannot check.
                if (suspend &&
                    interruptAtMessage != 0 &&
                    interruptAtMessage + 1 != maxMsgNum &&
                    report.size() == 1) {
                    for (const auto &sourceReport: report) {
                        CT_ASSERT(sourceReport.second.isResumeSync());
                    }
                }
            }
        }

        // copy changes to client A
        doSync(__FILE__, __LINE__, "toA", SyncOptions(SYNC_TWO_WAY));

        // compare client A and B
        if (interruptAtMessage != -1 &&
            !compareDatabases(refFileBase.c_str(), false)) {
            equal = false;
            std::cerr << "====> comparison of client B against reference file(s) failed after interrupting at message #" <<
                interruptAtMessage << std::endl;
            std::cerr.flush();
        }
        if (!compareDatabases(nullptr, false)) {
            equal = false;
            std::cerr << "====> comparison of client A and B failed after interrupting at message #" <<
                interruptAtMessage << std::endl;
            std::cerr.flush();
        }

        // save reference files from uninterrupted run?
        if (interruptAtMessage == -1) {
            for (source_it it = sources.begin();
                 it != sources.end();
                 ++it) {
                std::string refFile = refFileBase;
                refFile += it->second->config.m_sourceName;
                refFile += ".dat";
                simplifyFilename(refFile);
                TestingSyncSourcePtr source;
                SOURCE_ASSERT_NO_FAILURE(source.get(), source.reset(it->second->createSourceA()));
                SOURCE_ASSERT_EQUAL(source.get(), 0, it->second->config.m_dump(client, *source.get(), refFile.c_str()));
                CT_ASSERT_NO_THROW(source.reset());
            }
        }

        // pick next iteration
        if (requestedInterruptAt == -1) {
            // user requested to stop after first iteration
            break;
        } else if (requestedInterruptAt >= 0) {
            // only do one interrupted run of the test
            if (requestedInterruptAt == interruptAtMessage) {
                break;
            } else {
                interruptAtMessage = requestedInterruptAt;
            }
        } else {
            // interrupt one message later than before
            interruptAtMessage++;
            if (interrupt &&
                interruptAtMessage + 1 >= maxMsgNum) {
                // Don't interrupt before the server's last reply,
                // because then the server thinks we completed the
                // session when we think we didn't, which leads to a
                // slow sync. Testing that is better done with a
                // specific test.
                break;
            }
            if (interruptAtMessage >= maxMsgNum) {
                // next run would not interrupt at all, stop now
                break;
            }
        }
    }

    CT_ASSERT(equal);
}

void SyncTests::testInterruptResumeClientAdd()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_ADD, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testInterruptResumeClientRemove()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_REMOVE, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testInterruptResumeClientUpdate()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_UPDATE, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testInterruptResumeServerAdd()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_ADD, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testInterruptResumeServerRemove()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_REMOVE, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testInterruptResumeServerUpdate()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_UPDATE, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testInterruptResumeClientAddBig()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_ADD|BIG, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testInterruptResumeClientUpdateBig()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_UPDATE|BIG, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testInterruptResumeServerAddBig()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_ADD|BIG, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testInterruptResumeServerUpdateBig()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_UPDATE|BIG, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testInterruptResumeFull()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_ADD|CLIENT_REMOVE|CLIENT_UPDATE|
                                         SERVER_ADD|SERVER_REMOVE|SERVER_UPDATE, std::make_shared<TransportFaultInjector>()));
}

void SyncTests::testUserSuspendClientAdd()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_ADD, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testUserSuspendClientRemove()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_REMOVE, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testUserSuspendClientUpdate()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_UPDATE, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testUserSuspendServerAdd()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_ADD, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testUserSuspendServerRemove()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_REMOVE, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testUserSuspendServerUpdate()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_UPDATE, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testUserSuspendClientAddBig()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_ADD|BIG, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testUserSuspendClientUpdateBig()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_UPDATE|BIG, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testUserSuspendServerAddBig()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_ADD|BIG, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testUserSuspendServerUpdateBig()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_UPDATE|BIG, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testUserSuspendFull()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_ADD|CLIENT_REMOVE|CLIENT_UPDATE|
                                         SERVER_ADD|SERVER_REMOVE|SERVER_UPDATE, std::make_shared<UserSuspendInjector>()));
}

void SyncTests::testResendClientAdd()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_ADD, std::make_shared<TransportResendInjector>()));
}

void SyncTests::testResendClientRemove()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_REMOVE, std::make_shared<TransportResendInjector>()));
}

void SyncTests::testResendClientUpdate()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_UPDATE, std::make_shared<TransportResendInjector>()));
}

void SyncTests::testResendServerAdd()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_ADD, std::make_shared<TransportResendInjector>()));
}

void SyncTests::testResendServerRemove()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_REMOVE, std::make_shared<TransportResendInjector>()));
}

void SyncTests::testResendServerUpdate()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_UPDATE, std::make_shared<TransportResendInjector>()));
}

void SyncTests::testResendFull()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_ADD|CLIENT_REMOVE|CLIENT_UPDATE|
                                         SERVER_ADD|SERVER_REMOVE|SERVER_UPDATE, 
                                         std::make_shared<TransportResendInjector>()));
}

void SyncTests::testResendProxyClientAdd()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_ADD, std::make_shared<TransportResendProxy>()));
}

void SyncTests::testResendProxyClientRemove()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_REMOVE, std::make_shared<TransportResendProxy>()));
}

void SyncTests::testResendProxyClientUpdate()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_UPDATE, std::make_shared<TransportResendProxy>()));
}

void SyncTests::testResendProxyServerAdd()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_ADD, std::make_shared<TransportResendProxy>()));
}

void SyncTests::testResendProxyServerRemove()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_REMOVE, std::make_shared<TransportResendProxy>()));
}

void SyncTests::testResendProxyServerUpdate()
{
    CT_ASSERT_NO_THROW(doInterruptResume(SERVER_UPDATE, std::make_shared<TransportResendProxy>()));
}

void SyncTests::testResendProxyFull()
{
    CT_ASSERT_NO_THROW(doInterruptResume(CLIENT_ADD|CLIENT_REMOVE|CLIENT_UPDATE|
                                         SERVER_ADD|SERVER_REMOVE|SERVER_UPDATE, 
                                         std::make_shared<TransportResendProxy>()));
}

void SyncTests::testTimeout()
{
    // Create a dead listening socket, then run a sync with a sync URL
    // which points towards localhost at that port. Do this with no
    // message resending and a very short overall timeout. The
    // expectation is that the transmission timeout strikes.
    time_t start = time(nullptr);
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    CT_ASSERT(fd != -1);
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    int res = bind(fd, (sockaddr *)&servaddr, sizeof(servaddr));
    CT_ASSERT_EQUAL(0, res);
    socklen_t len = sizeof(servaddr);
    res = getsockname(fd, (sockaddr *)&servaddr, &len);
    CT_ASSERT_EQUAL(0, res);
    res = listen(fd, 10);
    CT_ASSERT_EQUAL(0, res);
    bool skipped = false;
    SyncReport report;
    auto setDeadSyncURL = [this, port=ntohs(servaddr.sin_port), &skipped] (SyncContext &context,
                                                                           SyncOptions &options) {
        vector<string> urls = context.getSyncURL();
        string url;
        if (urls.size() == 1) {
            url = urls.front();
        }

        // use IPv4 localhost address, that's what we listen on
        string fakeURL = StringPrintf("http://127.0.0.1:%d/foobar", port);

        if (boost::starts_with(url, "http")) {
            context.setSyncURL(fakeURL, true);
            context.setSyncUsername("foo", true);
            context.setSyncPassword("bar", true);
            return false;
        } else if (boost::starts_with(url, "local://")) {
            FullProps props = context.getConfigProps();
            string target = url.substr(strlen("local://"));
            props[target].m_syncProps["syncURL"] = fakeURL;
            props[target].m_syncProps["retryDuration"] = InitStateString("10", true);
            props[target].m_syncProps["retryInterval"] = InitStateString("10", true);
            context.setConfigProps(props);
            return false;
        } else {
            // cannot run test, tell parent
            skipped = true;
            return true;
        }
    };
    doSync(__FILE__, __LINE__,
           "timeout",
           SyncOptions(SYNC_SLOW,
                       CheckSyncReport(-1, -1, -1, -1, -1, -1,
                                       false).setReport(&report))
           .setPrepareCallback(setDeadSyncURL)
           .setRetryDuration(20)
           .setRetryInterval(20));
    time_t end = time(nullptr);
    close(fd);
    if (!skipped) {
        CT_ASSERT_EQUAL(STATUS_TRANSPORT_FAILURE, report.getStatus());
        std::string delta = StringPrintf("%lds", (long)(end - start));
        CT_ASSERT_MESSAGE(delta, end - start >= 19);
        // needs to be sufficiently larger than 20s timeout
        // because under valgrind the startup time is considerable
        CT_ASSERT_MESSAGE(delta, end - start < 50);
    }
}

static void UpdateLocal(const std::string &config, const std::string &source,
                        const std::string &actualLocalData,
                        const std::string &localModified,
                        const std::string &modifyLocal)
{
    // The local side also uses the Cmdline class because then we only
    // need to implement one way of updating items. But first we need to
    // get the actual data.
    std::unique_ptr<Cmdline> cmdline;

    rm_r(actualLocalData);
    mkdir_p(actualLocalData);
    cmdline.reset(new TestCmdline("--daemon=no",
                                  "--export",
                                  actualLocalData.c_str(),
                                  config.c_str(),
                                  source.c_str(),
                                  nullptr));
    CT_ASSERT(cmdline->parse());
    CT_ASSERT_MESSAGE("export " + currentServer() + "_1 " +  source, cmdline->run());

    CT_ASSERT(!system(StringPrintf("%s %s %s",
                                   modifyLocal.c_str(),
                                   actualLocalData.c_str(),
                                   localModified.c_str()).c_str()));
    CT_ASSERT(isDir(localModified));
    cmdline.reset(new TestCmdline("--daemon=no",
                                  "--update",
                                  localModified.c_str(),
                                  config.c_str(),
                                  source.c_str(),
                                  nullptr));
    CT_ASSERT(cmdline->parse());
    CT_ASSERT_MESSAGE("update " + config + " " +  source, cmdline->run());
}

void SyncTests::testUpload()
{
    const std::string testname = "testUpload";

    CT_ASSERT_EQUAL(sources.size(), 1);
    const ClientTest::Config &config(sources[0].second->config);
    StringPair peerConfig = getPeerConfig(config.m_sourceName);
    const std::string &peer = peerConfig.first;
    const std::string &peerSource = peerConfig.second;
    CT_ASSERT(!peer.empty());
    CT_ASSERT(!peerSource.empty());

    std::string localTestdata = getPeerTestdata(config.m_sourceName, testname, "local");
    CT_ASSERT_MESSAGE(localTestdata, !access(localTestdata.c_str(), R_OK));
    std::string remoteTestdata = getPeerTestdata(config.m_sourceName, testname, "remote");
    CT_ASSERT_MESSAGE(remoteTestdata, !access(remoteTestdata.c_str(), R_OK));
    std::string modifyRemote = getPeerTestdata(config.m_sourceName, testname, "modify-remote");
    CT_ASSERT_MESSAGE(modifyRemote, !access(modifyRemote.c_str(), R_OK|X_OK));
    std::string localSyncedTestdata = getPeerTestdata(config.m_sourceName, testname, "local-synced");
    CT_ASSERT_MESSAGE(localSyncedTestdata, !access(localSyncedTestdata.c_str(), R_OK));

    std::unique_ptr<Cmdline> cmdline;

    // Import locally into empty database.
    sources[0].second->deleteAll(sources[0].second->createSourceA);
    sources[0].second->doImport(localTestdata);

    // Sync to remote.
    doSync(__FILE__, __LINE__,
           "upload",
           SyncOptions(RefreshFromLocalMode(),
                       CheckSyncReport(0,0,0, -1,0,0, true, SYNC_REFRESH_FROM_LOCAL)));

    // Export from remote directly.
    std::string actualData = getCurrentTest() + ".remote.test.dat";
    simplifyFilename(actualData);
    mkdir_p(actualData);
    cmdline.reset(new TestCmdline("--daemon=no",
                                  "--export",
                                  actualData.c_str(),
                                  peer.c_str(),
                                  peerSource.c_str(),
                                  nullptr));
    CT_ASSERT(cmdline->parse());
    CT_ASSERT_MESSAGE(peer + " " + peerSource, cmdline->run());

    // Compare against expected result. We use the compare operation
    // of the local source and apply it to data from the remote one.
    // This typically works if the data has the same format.
    {
        ScopedEnvChange fullSyncCompare("CLIENT_TEST_SERVER", "none");
        CT_ASSERT(sources[0].second->compareDatabases(remoteTestdata, actualData));
    }

    // Modify remotely.
    std::string remoteModified = getCurrentTest() + ".remote.modified.test.dat";
    simplifyFilename(remoteModified);
    CT_ASSERT(!system(StringPrintf("%s %s %s",
                                   modifyRemote.c_str(),
                                   actualData.c_str(),
                                   remoteModified.c_str()).c_str()));
    CT_ASSERT(isDir(remoteModified));
    cmdline.reset(new TestCmdline("--daemon=no",
                                  "--update",
                                  remoteModified.c_str(),
                                  peer.c_str(),
                                  peerSource.c_str(),
                                  nullptr));
    CT_ASSERT(cmdline->parse());
    CT_ASSERT_MESSAGE("update " + peer + " " + peerSource, cmdline->run());

    // Sync between both sides to update the local data.
    doSync(__FILE__, __LINE__,
           "two-way",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,-1,0, 0,0,0, true, SYNC_TWO_WAY)));

    // Compare against expected result.
    TestingSyncSourcePtr copy;
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(sources[0].second->createSourceA(), TestingSyncSourcePtr::SLOW));
    {
        ScopedEnvChange fullSyncCompare("CLIENT_TEST_SERVER", "none");
        CT_ASSERT(sources[0].second->compareDatabases(localSyncedTestdata.c_str(), *copy));
    }
}

void SyncTests::testDownload()
{
    const std::string testname = "testDownload";

    CT_ASSERT_EQUAL(sources.size(), 1);
    const ClientTest::Config &config(sources[0].second->config);
    StringPair peerConfig = getPeerConfig(config.m_sourceName);
    const std::string &peer = peerConfig.first;
    const std::string &peerSource = peerConfig.second;
    CT_ASSERT(!peer.empty());
    CT_ASSERT(!peerSource.empty());

    std::string localTestdata = getPeerTestdata(config.m_sourceName, testname, "local");
    CT_ASSERT_MESSAGE(localTestdata, !access(localTestdata.c_str(), R_OK));
    std::string remoteTestdata = getPeerTestdata(config.m_sourceName, testname, "remote");
    CT_ASSERT_MESSAGE(remoteTestdata, !access(remoteTestdata.c_str(), R_OK));
    std::string modifyLocal = getPeerTestdata(config.m_sourceName, testname, "modify-local");
    CT_ASSERT_MESSAGE(modifyLocal, !access(modifyLocal.c_str(), R_OK|X_OK));
    std::string remoteSyncedTestdata = getPeerTestdata(config.m_sourceName, testname, "remote-synced");
    CT_ASSERT_MESSAGE(remoteSyncedTestdata, !access(remoteSyncedTestdata.c_str(), R_OK));

    std::unique_ptr<Cmdline> cmdline;

    // Wipe remote directly, then import.
    cmdline.reset(new TestCmdline("--daemon=no",
                                  "--delete-items",
                                  peer.c_str(),
                                  peerSource.c_str(),
                                  "*",
                                  nullptr));
    CT_ASSERT(cmdline->parse());
    CT_ASSERT_MESSAGE(peer + " " + peerSource, cmdline->run());
    cmdline.reset(new TestCmdline("--daemon=no",
                                  "--import",
                                  remoteTestdata.c_str(),
                                  peer.c_str(),
                                  peerSource.c_str(),
                                  nullptr));
    CT_ASSERT(cmdline->parse());
    CT_ASSERT_MESSAGE(peer + " " + peerSource, cmdline->run());

    // Sync into local database.
    doSync(__FILE__, __LINE__,
           "download",
           SyncOptions(SYNC_REFRESH_FROM_REMOTE,
                       CheckSyncReport(-1,0,-1, 0,0,0, true, SYNC_REFRESH_FROM_REMOTE)));

    // Compare against expected result.
    TestingSyncSourcePtr copy;
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(sources[0].second->createSourceA(), TestingSyncSourcePtr::SLOW));
    {
        ScopedEnvChange fullSyncCompare("CLIENT_TEST_SERVER", "none");
        CT_ASSERT(sources[0].second->compareDatabases(localTestdata.c_str(), *copy));
    }

    // Modify locally.
    std::string actualLocalData = getCurrentTest() + ".local.test.dat";
    simplifyFilename(actualLocalData);
    std::string localModified = getCurrentTest() + ".local.modified.test.dat";
    simplifyFilename(localModified);
    CT_ASSERT_NO_THROW(UpdateLocal(currentServer() + "_1", config.m_sourceName,
                                   actualLocalData, localModified,
                                   modifyLocal));

    // Sync between both sides to update the remote.
    doSync(__FILE__, __LINE__,
           "two-way",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,0,0, 0,-1,0, true, SYNC_TWO_WAY)));

    // Check remote.
    std::string syncedRemoteData = getCurrentTest() + ".remote.test.dat";
    simplifyFilename(syncedRemoteData);
    rm_r(syncedRemoteData);
    mkdir_p(syncedRemoteData);
    cmdline.reset(new TestCmdline("--daemon=no",
                                  "--export",
                                  syncedRemoteData.c_str(),
                                  peer.c_str(),
                                  peerSource.c_str(),
                                  nullptr));
    CT_ASSERT(cmdline->parse());
    CT_ASSERT_MESSAGE("export " + peer + " " + peerSource, cmdline->run());

    // Compare against expected result. We use the compare operation
    // of the local source and apply it to data from the remote one.
    // This typically works if the data has the same format.
    {
        ScopedEnvChange fullSyncCompare("CLIENT_TEST_SERVER", "none");
        CT_ASSERT(sources[0].second->compareDatabases(remoteSyncedTestdata, syncedRemoteData));
    }
}

void SyncTests::doUpdateConflict(const std::string &testname, bool localWins)
{
    CT_ASSERT_EQUAL(sources.size(), 1);
    const ClientTest::Config &config(sources[0].second->config);
    StringPair peerConfig = getPeerConfig(config.m_sourceName);
    const std::string &peer = peerConfig.first;
    const std::string &peerSource = peerConfig.second;
    CT_ASSERT(!peer.empty());
    CT_ASSERT(!peerSource.empty());

    std::string localTestdata = getPeerTestdata(config.m_sourceName, testname, "local");
    CT_ASSERT_MESSAGE(localTestdata, !access(localTestdata.c_str(), R_OK));
    std::string localSyncedTestdata = getPeerTestdata(config.m_sourceName, testname, "local-synced");
    CT_ASSERT_MESSAGE(localSyncedTestdata, !access(localSyncedTestdata.c_str(), R_OK));
    std::string remoteSyncedTestdata = getPeerTestdata(config.m_sourceName, testname, "remote-synced");
    CT_ASSERT_MESSAGE(remoteSyncedTestdata, !access(remoteSyncedTestdata.c_str(), R_OK));
    std::string modifyLocal = getPeerTestdata(config.m_sourceName, testname, "modify-local");
    CT_ASSERT_MESSAGE(modifyLocal, !access(modifyLocal.c_str(), R_OK|X_OK));
    std::string modifyRemote = getPeerTestdata(config.m_sourceName, testname, "modify-remote");
    CT_ASSERT_MESSAGE(modifyRemote, !access(modifyRemote.c_str(), R_OK|X_OK));

    // Import locally into empty database.
    sources[0].second->deleteAll(sources[0].second->createSourceA);
    sources[0].second->doImport(localTestdata);

    // Sync to remote.
    doSync(__FILE__, __LINE__,
           "upload",
           SyncOptions(RefreshFromLocalMode(),
                       CheckSyncReport(0,0,0, -1,0,0, true, SYNC_REFRESH_FROM_LOCAL)));

    // Export from remote directly.
    std::string actualRemoteData = getCurrentTest() + ".remote.test.dat";
    simplifyFilename(actualRemoteData);
    std::unique_ptr<Cmdline> cmdline;
    rm_r(actualRemoteData);
    mkdir_p(actualRemoteData);
    cmdline.reset(new TestCmdline("--daemon=no",
                                  "--export",
                                  actualRemoteData.c_str(),
                                  peer.c_str(),
                                  peerSource.c_str(),
                                  nullptr));
    CT_ASSERT(cmdline->parse());
    CT_ASSERT_MESSAGE("export " + peer + " " + peerSource, cmdline->run());

    // Modify all items on both sides. In both cases the modification
    // is done with a shell script which must make a copy of the data.
    // The shell scripts can be used to cause one or the other side
    // to have a modified version of an item or both at the same time,
    // which will trigger merging in the engine. The shell script
    // needs to create an entry for each item which is meant to be
    // updated, using the same file name (= luid) as in the input
    // directory.
    //
    // The order and timing of updating matters for the test because
    // the engine will look at time stamps (REV resp. LAST-MODIFIED)
    // to determine which side has the more recent change.

    for (int i = 0; i < 2; i++) {
        if (localWins ? i == 0 : i == 1) {
            // The remote side can use the data downloaded earlier.
            std::string remoteModified = getCurrentTest() + ".remote.modified.test.dat";
            simplifyFilename(remoteModified);
            CT_ASSERT(!system(StringPrintf("%s %s %s",
                                           modifyRemote.c_str(),
                                           actualRemoteData.c_str(),
                                           remoteModified.c_str()).c_str()));
            CT_ASSERT(isDir(remoteModified));
            cmdline.reset(new TestCmdline("--daemon=no",
                                          "--update",
                                          remoteModified.c_str(),
                                          peer.c_str(),
                                          peerSource.c_str(),
                                          nullptr));
            CT_ASSERT(cmdline->parse());
            CT_ASSERT_MESSAGE("update " + peer + " " + peerSource, cmdline->run());

            // Check remote after update.
            std::string remoteActualModified = getCurrentTest() + ".remote.actual.test.dat";
            simplifyFilename(remoteActualModified);
            rm_r(remoteActualModified);
            mkdir_p(remoteActualModified);
            cmdline.reset(new TestCmdline("--daemon=no",
                                          "--export",
                                          remoteActualModified.c_str(),
                                          peer.c_str(),
                                          peerSource.c_str(),
                                          nullptr));
            CT_ASSERT(cmdline->parse());
            CT_ASSERT_MESSAGE("export " + peer + " " + peerSource, cmdline->run());

            // Copy all unmodified items before the comparison.
            ReadDir dir(remoteModified);
            std::set<std::string> modified(dir.begin(), dir.end());
            for (const std::string &luid: ReadDir(actualRemoteData)) {
                if (modified.find(luid) == modified.end()) {
                    std::string content;
                    CT_ASSERT(ReadFile(actualRemoteData + "/" + luid, content));
                    std::ofstream((remoteModified + "/" + luid).c_str()).write(content.c_str(), content.size());
                }
            }

            // Compare against expected result. We use the compare operation
            // of the local source and apply it to data from the remote one.
            // This typically works if the data has the same format.
            {
                ScopedEnvChange fullSyncCompare("CLIENT_TEST_SERVER", "none");
                CT_ASSERT(sources[0].second->compareDatabases(remoteModified, remoteActualModified));
            }
        } else {
            std::string actualLocalData = getCurrentTest() + ".local.test.dat";
            simplifyFilename(actualLocalData);
            std::string localModified = getCurrentTest() + ".local.modified.test.dat";
            simplifyFilename(localModified);
            CT_ASSERT_NO_THROW(UpdateLocal(currentServer() + "_1", config.m_sourceName,
                                           actualLocalData, localModified,
                                           modifyLocal));
        }

        // System time must be synchronized with the remote side for the
        // test to pass reliably. Wait here and/or check that
        // loosing side's time is in the past (TODO).
        sleep(5);
    }

    // Sync between both sides.
    doSync(__FILE__, __LINE__,
           "two-way",
           SyncOptions(SYNC_TWO_WAY,
                       CheckSyncReport(0,-1,-1, 0,-1,0, true, SYNC_TWO_WAY)));

    // Check remote.
    std::string syncedRemoteData = getCurrentTest() + ".remote.test.dat";
    simplifyFilename(syncedRemoteData);
    rm_r(syncedRemoteData);
    mkdir_p(syncedRemoteData);
    cmdline.reset(new TestCmdline("--daemon=no",
                                  "--export",
                                  syncedRemoteData.c_str(),
                                  peer.c_str(),
                                  peerSource.c_str(),
                                  nullptr));
    CT_ASSERT(cmdline->parse());
    CT_ASSERT_MESSAGE("export " + peer + " " + peerSource, cmdline->run());

    // Compare against expected result. We use the compare operation
    // of the local source and apply it to data from the remote one.
    // This typically works if the data has the same format.
    {
        ScopedEnvChange fullSyncCompare("CLIENT_TEST_SERVER", "none");
        CT_ASSERT(sources[0].second->compareDatabases(remoteSyncedTestdata, syncedRemoteData));
    }

    // Check local.
    TestingSyncSourcePtr copy;
    SOURCE_ASSERT_NO_FAILURE(copy.get(), copy.reset(sources[0].second->createSourceA(), TestingSyncSourcePtr::SLOW));
    {
        ScopedEnvChange fullSyncCompare("CLIENT_TEST_SERVER", "none");
        CT_ASSERT(sources[0].second->compareDatabases(localSyncedTestdata.c_str(), *copy));
    }
}

void SyncTests::testUpdateRemoteWins()
{
    // Local side gets updated first, then remote -> remote wins during merge conflict.
    doUpdateConflict("testUpdateRemoteWins", false);
}

void SyncTests::testUpdateLocalWins()
{
    // Remote side gets updated first, then local -> local wins during merge conflict.
    doUpdateConflict("testUpdateLocalWins", true);
}

void SyncTests::doSync(const SyncOptions &options)
{
    int res = 0;
    static int syncCounter = 0;
    static std::string lastTest;
    std::stringstream logstream;

    // reset counter when switching tests
    if (lastTest != getCurrentTest()) {
        syncCounter = 0;
        lastTest = getCurrentTest();
    }

    std::string prefix;
    prefix.reserve(80);
    for (auto it = logPrefixes.begin();
         it != logPrefixes.end();
         ++it) {
        prefix += ".";
        prefix += *it;
    }
    if (!prefix.empty()) {
        printf(" %s", prefix.c_str() + 1);
        fflush(stdout);
    }

    logstream /* << std::setw(4) << std::setfill('0') << syncCounter << "_" */ << getCurrentTest()
                 << prefix
                 << ".client." << (accessClientB ? "A" : "B");
    std::string logname = logstream.str();
    simplifyFilename(logname);
    syncCounter++;

    SE_LOG_DEBUG(NULL, "%d. starting %s with sync mode %s",
                 syncCounter, logname.c_str(), PrettyPrintSyncMode(options.m_syncMode).c_str());

    try {
        CT_ASSERT_NO_THROW(res = client.doSync(sourceArray,
                                               logname,
                                               options));
    } catch (...) {
        postSync(res, logname);
        throw;
    }
    CT_ASSERT_NO_THROW(postSync(res, logname));
}

void SyncTests::postSync(int res, const std::string &logname)
{
    client.postSync(res, logname);
}

void SyncTests::allSourcesInsert(bool withUID)
{
    for (auto &source_pair: sources)  {
        CT_ASSERT_NO_THROW(source_pair.second->doInsert(withUID));
    }
}

void SyncTests::allSourcesUpdate()
{
    for (auto &source_pair: sources)  {
        CT_ASSERT_NO_THROW(source_pair.second->update(source_pair.second->createSourceA,
                                                      source_pair.second->config.m_updateItem));
    }
}

void SyncTests::allSourcesDeleteAll()
{
    for (auto &source_pair: sources)  {
        CT_ASSERT_NO_THROW(source_pair.second->deleteAll(source_pair.second->createSourceA));
    }
}

void SyncTests::allSourcesInsertMany(int startIndex, int numItems,
                                     std::map<int, std::list<std::string> > &luids)
{
    for (auto &source_pair: sources)  {
        std::list<std::string> l;
        CT_ASSERT_NO_THROW(l = source_pair.second->insertManyItems(source_pair.second->createSourceA,
                                                                   startIndex,
                                                                   numItems,
                                                                   0));
        CT_ASSERT_EQUAL((size_t)numItems, l.size());
        // append instead of overwriting - useful when multiple
        // insertMany calls share the same luid buffer
        luids[source_pair.first].insert(luids[source_pair.first].end(), l.begin(), l.end());
    }
}

void SyncTests::allSourcesUpdateMany(int startIndex, int numItems,
                                     int revision,
                                     std::map<int, std::list<std::string> > &luids,
                                     int offset)
{
    for (auto &source_pair: sources)  {
        CT_ASSERT_NO_THROW(source_pair.second->updateManyItems(source_pair.second->createSourceA,
                                                               startIndex,
                                                               numItems,
                                                               0,
                                                               revision,
                                                               luids[source_pair.first],
                                                               offset));
    }
}

void SyncTests::allSourcesRemoveMany(int numItems,
                                     std::map<int, std::list<std::string> > &luids,
                                     int offset)
{
    for (auto &source_pair: sources)  {
        CT_ASSERT_NO_THROW(source_pair.second->removeManyItems(source_pair.second->createSourceA,
                                                               numItems,
                                                               luids[source_pair.first],
                                                               offset));
    }
}


/** generates tests on demand based on what the client supports */
class ClientTestFactory : public CppUnit::TestFactory {
public:
    ClientTestFactory(ClientTest &c) :
        client(c) {}

    virtual CppUnit::Test *makeTest() {
        int source;
        CppUnit::TestSuite *alltests = new CppUnit::TestSuite("Client");
        CppUnit::TestSuite *tests;

        // create local source tests
        typedef std::map<std::string, LocalTests *> ConfigMap;
        ConfigMap configs;
        tests = new CppUnit::TestSuite(alltests->getName() + "::Source");
        for (source=0; source < client.getNumLocalSources(); source++) {
            ClientTest::Config config;
            client.getLocalSourceConfig(source, config);
            if (!config.m_sourceName.empty()) {
                LocalTests *sourcetests =
                    client.createLocalTests(tests->getName() + "::" + config.m_sourceName, source, config);
                sourcetests->addTests();
                tests->addTest(FilterTest(sourcetests));
                configs[config.m_sourceName] = sourcetests;
            }
        }
        // link configs of sources which share the same database
        for (const auto &entry: configs) {
            LocalTests *sourcetests = entry.second;
            const ClientTest::Config &config = sourcetests->config;
            if (!config.m_linkedSources.empty()) {
                sourcetests->m_linkedSources.push_back(sourcetests);
                for (const std::string &source: config.m_linkedSources) {
                    sourcetests->m_linkedSources.push_back(configs[source]);
                }
            }
        }
        alltests->addTest(FilterTest(tests));

        // create sync tests with just one source
        tests = new CppUnit::TestSuite(alltests->getName() + "::Sync");
        for (source=0; source < client.getNumSyncSources(); source++) {
            ClientTest::Config config;
            client.getSyncSourceConfig(source, config);
            if (!config.m_sourceName.empty()) {
                std::vector<int> sources;
                sources.push_back(source);
                SyncTests *synctests =
                    client.createSyncTests(tests->getName() + "::" + config.m_sourceName, sources);
                synctests->addTests(source == 0);
                tests->addTest(FilterTest(synctests));
            }
        }

        // create sync tests with all sources enabled, unless we only have one:
        // that would be identical to the test above
        std::vector<int> sources;
        std::string name, name_reversed;
        for (source=0; source < client.getNumSyncSources(); source++) {
            ClientTest::Config config;
            client.getSyncSourceConfig(source, config);
            if (!config.m_sourceName.empty()) {
                sources.push_back(source);
                if (name.size() > 0) {
                    name += "_";
                    name_reversed = std::string("_") + name_reversed;
                }
                name += config.m_sourceName;
                name_reversed = config.m_sourceName + name_reversed;
            }
        }
        if (sources.size() > 1) {
            SyncTests *synctests =
                client.createSyncTests(tests->getName() + "::" + name, sources);
            synctests->addTests();
            tests->addTest(FilterTest(synctests));
            synctests = 0;

            if (getenv("CLIENT_TEST_REVERSE_SOURCES")) {
                // now also in reversed order - who knows, it might make a difference;
                // typically it just makes the whole run slower, so not enabled
                // by default
                std::reverse(sources.begin(), sources.end());
                synctests =
                    client.createSyncTests(tests->getName() + "::" + name_reversed, sources);
                synctests->addTests();
                tests->addTest(FilterTest(synctests));
                synctests = 0;
            }
        }

        alltests->addTest(FilterTest(tests));
        tests = 0;

        return alltests;
    }

private:
    ClientTest &client;
};

void ClientTest::registerTests()
{
    freeFactory();
    factory = (void *)new ClientTestFactory(*this);
    CppUnit::TestFactoryRegistry::getRegistry().registerFactory((CppUnit::TestFactory *)factory);
}

ClientTest::ClientTest(int serverSleepSec, const std::string &serverLog) :
    serverSleepSeconds(serverSleepSec),
    serverLogFileName(serverLog),
    factory(nullptr)
{
}

void ClientTest::freeFactory()
{
    if(factory) {
        CppUnit::TestFactoryRegistry::getRegistry().unregisterFactory((CppUnit::TestFactory *)factory);
        delete (CppUnit::TestFactory *)factory;
        factory = nullptr;
    }
}

ClientTest::~ClientTest()
{
    freeFactory();
}

void ClientTest::registerCleanup(Cleanup_t cleanup)
{
    cleanupSet.insert(cleanup);
}

void ClientTest::shutdown()
{
    for (Cleanup_t cleanup: cleanupSet) {
        cleanup();
    }
}

LocalTests *ClientTest::createLocalTests(const std::string &name, int sourceParam, ClientTest::Config &co)
{
    return new LocalTests(name, *this, sourceParam, co);
}

SyncTests *ClientTest::createSyncTests(const std::string &name, std::vector<int> sourceIndices, bool isClientA)
{
    return new SyncTests(name, *this, sourceIndices, isClientA);
}

int ClientTest::dump(ClientTest &client, TestingSyncSource &source, const std::string &file)
{
    BackupReport report;
    auto node = std::make_shared<VolatileConfigNode>();

    rm_r(file);
    mkdir_p(file);
    CT_ASSERT(source.getOperations().m_backupData);
    source.getOperations().m_backupData(SyncSource::Operations::ConstBackupInfo(),
                                        SyncSource::Operations::BackupInfo(SyncSource::Operations::BackupInfo::BACKUP_OTHER, file, node),
                                        report);
    return 0;
}

void ClientTest::getItems(const std::string &file, list<string> &items, std::string &testcases)
{
    items.clear();

    // import the file, trying a .tem file (base file plus patch)
    // first
    std::ifstream input;
    string server = currentServer();
    testcases = file + '.' + server +".tem";
    input.open(testcases.c_str());

    if (input.fail()) {
        // try server-specific file (like eds_event.ics.local)
        testcases = file + '.' + server;
        input.open(testcases.c_str());
    }

    if (input.fail()) {
        // try base file
        testcases = file;
        input.open(testcases.c_str());
    }
    CT_ASSERT(!input.bad());
    CT_ASSERT(input.is_open());
    std::string data, line;
    while (input) {
        bool wasend = false;
        do {
            getline(input, line);
            CT_ASSERT(!input.bad());
            // empty lines directly after line which starts with END mark end of record;
            // check for END necessary becayse vCard 2.1 ENCODING=BASE64 may have empty lines in body of VCARD!
            if ((line != "\r" && line.size() > 0) || !wasend) {
                data += line;
                data += "\n";
            } else {
                if (!data.empty()) {
                    items.push_back(data);
                }
                data = "";
            }
            wasend = !line.compare(0, 4, "END:");
        } while(!input.eof());
    }
    if (data != "" && data != "\r\n" && data != "\n") {
        items.push_back(data);
    }
}

std::string ClientTest::import(ClientTest &client, TestingSyncSource &source, const ClientTestConfig &config,
                               const std::string &file, std::string &realfile,
                               std::list<std::string> *luids)
{
    list<string> items;
    getItems(file, items, realfile);
    SE_LOG_DEBUG(NULL, "importing %d test cases from file %s", (int)items.size(), realfile.c_str());
    std::string failures;
    bool doImport = !luids || luids->empty();
    std::list<std::string>::const_iterator it;
    if (!doImport) {
        it = luids->begin();
    }
    for (string &data: items) {
        std::string luid;
        try {
            if (doImport) {
                luid = importItem(&source, config, data);
                CT_ASSERT(!luid.empty());
                if (luids) {
                    luids->push_back(luid);
                }
            } else {
                CT_ASSERT(it != luids->end());
                luid = *it;
                ++it;
                // Did import already fail? If yes, then don't try to
                // update because it will also fail.
                if (!luid.empty()) {
                    // TODO: should fail for status = 6 in eas
                    updateItem(&source, data, luid);
                }
            }
        } catch (...) {
            std::string explanation;
            Exception::handle(explanation);
            failures += "Failed to ";
            if (doImport) {
                failures += "import:\n";
            } else {
                failures += "update " + luid + ":\n";
            }
            failures += data;
            failures += "\n";
            failures += explanation;
            failures += "\n";
            if (doImport && luids) {
                luids->push_back("");
            }
        }
    }
    return failures;
}

bool ClientTest::compare(ClientTest &client, const std::string &fileA, const std::string &fileB)
{
    setenv("CLIENT_TEST_HEADER", "\n\n", 1);
    setenv("CLIENT_TEST_LEFT_NAME", fileA.c_str(), 1);
    setenv("CLIENT_TEST_RIGHT_NAME", fileB.c_str(), 1);
    setenv("CLIENT_TEST_REMOVED", "only in left file", 1);
    setenv("CLIENT_TEST_ADDED", "only in right file", 1);
    bool success = false;
    const char* compareLog = getenv("CLIENT_TEST_COMPARE_LOG");
    if(compareLog && strlen(compareLog))
    {
       std::string cmdstr = std::string("synccompare ") + fileA + " " + fileB;
       string tmpfile = "____compare.txt";
       cmdstr =string("bash -c 'set -o pipefail;") + cmdstr;
       cmdstr += " 2>&1|tee " +tmpfile+"'";
       success = system(cmdstr.c_str()) == 0;
    } else {
        // Shortcut: run synccompare directly, without shell in the middle
        // (reduces overhead and output when running under valgrind).
        pid_t child = fork();
        switch (child) {
        case -1:
            perror("fork");
            break;
        case 0:
            // child
            execl("synccompare", "synccompare", fileA.c_str(), fileB.c_str(), nullptr);
            perror("synccompare");
            exit(1);
            break;
        default:
            // parent
            int status;
            child = waitpid(child, &status, 0);
            if (child == -1) {
                perror("wait for synccompare");
            } else if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
                success = true;
            }
            break;
        }
    }
    if (!success) {
        printf("failed: env CLIENT_TEST_SERVER=%s 'CLIENT_TEST_STRIP_PARAMETERS=%s' 'CLIENT_TEST_STRIP_PROPERTIES=%s'synccompare %s %s\n",
               getEnv("CLIENT_TEST_SERVER", ""),
               getEnv("CLIENT_TEST_STRIP_PARAMETERS", ""),
               getEnv("CLIENT_TEST_STRIP_PROPERTIES", ""),
               fileA.c_str(), fileB.c_str());
    }
    return success;
}

void ClientTest::update(std::string &item)
{
    const static char *props[] = {
        "\nSUMMARY",
        "\nNOTE",
        nullptr
    };

    for (const char **prop = props; *prop; prop++) {
        size_t pos;
        pos = item.find(*prop);
        if (pos != item.npos) {
            // Modify existing property. Fast-forward to : (works as
            // long as colon is not in parameters).
            pos = item.find(':', pos);
        }
        if (pos != item.npos) {
            item.insert(pos + 1, "MOD-");
        } else if (!strcmp(*prop, "\nNOTE") && (pos = item.find("END:VCARD")) != item.npos) {
            // add property, but only if it is allowed in the item
            item.insert(pos, "NOTE:MOD\n");
        }
    }
}

void ClientTest::postSync(int res, const std::string &logname)
{
#ifdef WIN32
    Sleep(serverSleepSeconds * 1000);
#else
    sleep(serverSleepSeconds);

    // make a copy of the server's log (if found), then truncate it
    if (serverLogFileName.size()) {
        int fd = open(serverLogFileName.c_str(), O_RDWR);

        if (fd >= 0) {
            int out = open((logname + ".server.txt").c_str(), O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);
            if (out >= 0) {
                char buffer[4096];
                bool cont = true;
                ssize_t len;
                while (cont && (len = read(fd, buffer, sizeof(buffer))) > 0) {
                    ssize_t total = 0;
                    while (cont && total < len) {
                        ssize_t written = write(out, buffer, len);
                        if (written < 0) {
                            perror(("writing " + logname + ".server.txt").c_str());
                            cont = false;
                        } else {
                            total += written;
                        }
                    }
                }
                if (len < 0) {
                    perror(("reading " + serverLogFileName).c_str());
                }
                close(out);
            }
            if (ftruncate(fd, 0)) {
                perror("truncating log file");
            }
            close(fd);
        } else {
            perror(serverLogFileName.c_str());
        }
    }
#endif
}

static string mangleGeneric(const std::string &data, bool update, const std::string &uniqueUIDSuffix)
{
    std::string item = data;
    if (update) {
        boost::replace_first(item, "NOTE:", "NOTE:U ");
    }
    return item;
}

static string mangleICalendar20(const std::string &data, bool update, const std::string &uniqueUIDSuffix)
{
    std::string item = data;

    if (update) {
        static const std::regex re("BEGIN:(VEVENT|VJOURNAL|VTODO)\n");
        std::smatch match;
        std::regex_search(data, match, re);
        auto type = match[1];
        if (type.str() == "VJOURNAL") {
            // Need to modify first line of description and summary
            // consistently for a note because in plain text
            // representation, these lines are expected to be
            // identical.
            boost::replace_first(item, "SUMMARY:", "SUMMARY:U ");
        }
        boost::replace_first(item, "DESCRIPTION:", "DESCRIPTION:U ");
    }

    if (getenv("CLIENT_TEST_NO_UID")) {
        stripProperty(item, "UID");
    } else if (getenv("CLIENT_TEST_SIMPLE_UID")) {
        boost::replace_all(item, "UID:1234567890!@#$%^&*()<>@dummy", "UID:1234567890@dummy");
    }

    const char *uniqueUID = getenv("CLIENT_TEST_UNIQUE_UID");
    if (uniqueUID) {
        // Making UID unique per test to avoid issues
        // when the source already holds older copies.
        // Might still be an issue in real life?!
        static time_t start;
        static std::string test;
        if (test != getCurrentTest()) {
            start = time(nullptr);
            test = getCurrentTest();
        }
        std::string unique = StringPrintf("UID:UNIQUE-UID-%llu-", (long long unsigned)start);
        boost::replace_all(item, "UID:", unique);
        if (atoi(uniqueUID) > 1) {
            // Also avoid reusing the same UID inside the same test.
            // Required by Google CalDAV in calendar testChanges, because
            // they keep even deleted items around and check the SEQUENCE
            // number against their old data.
            boost::replace_all(item, "UNIQUE-UID", "UNIQUE-UID" + uniqueUIDSuffix);
        }
    } else if (getenv("CLIENT_TEST_LONG_UID")) {
        boost::replace_all(item, "UID:", "UID:this-is-a-ridiculously-long-uid-");
    }

    size_t offset = item.find("\nLAST-MODIFIED:");
    static const size_t len = strlen("\nLAST-MODIFIED:20100131T235959Z");
    if (offset != item.npos) {
        // Special semantic for iCalendar 2.0: LAST-MODIFIED should be
        // incremented in updated items. Emulate that by inserting the
        // current time.
        time_t now = time(nullptr);
        struct tm tm;
        gmtime_r(&now, &tm);
        std::string mod = StringPrintf("\nLAST-MODIFIED:%04d%02d%02dT%02d%02d%02dZ",
                                       tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
                                       tm.tm_hour, tm.tm_min, tm.tm_sec);
        item.replace(offset, len, mod);
    }

    const static string sequence("\nSEQUENCE:XXX");
    offset = item.find(sequence);
    if (offset != item.npos) {
        if (getenv("CLIENT_TEST_INCREASE_SEQUENCE")) {
            // Increment sequence number in steps of 100 to ensure that our
            // new item is considered more recent than any corresponding
            // item in the source. Some storages (Google CalDAV) check that.
            static int counter = 100;
            item.replace(offset, sequence.size(), StringPrintf("\nSEQUENCE:%d", counter));
            counter += 100;
        } else {
            item.replace(offset, sequence.size(), "\nSEQUENCE:1");
        }
    }

    return item;
}


// instead of trying to determine the dates of all Sundays in 2012
// algorithmically, hard-code them...
static const struct {
    int m_month, m_day;
} sundays[] = {
    { 1, 1 },
    { 1, 8 },
    { 1, 15 },
    { 1, 22 },
    { 1, 29 },
    { 2, 5 },
    { 2, 12 },
    { 2, 19 },
    { 2, 26 },
    { 3, 4 },
    { 3, 11 },
    { 3, 18 },
    // winter time ends on March 25th, week 12 (counting from zero)
#define SUNDAYS_2012_WINTER_TIME_ENDS 12
    { 3, 25 },
    { 4, 1 },
    { 4, 8 },
    { 4, 15 },
    { 4, 22 },
    { 4, 29 },
    { 5, 6 },
    { 5, 13 },
    { 5, 20 },
    { 5, 27 },
    { 6, 3 },
    { 6, 10 },
    { 6, 17 },
    { 6, 24 },
    { 7, 1 },
    { 7, 8 },
    { 7, 15 },
    { 7, 22 },
    { 7, 29 },
    { 8, 5 },
    { 8, 12 },
    { 8, 19 },
    { 8, 26 },
    { 9, 2 },
    { 9, 9 },
    { 9, 16 },
    { 9, 23 },
    { 9, 30 },
    { 10, 7 },
    { 10, 14 },
    { 10, 21 },
    // winter time start on October 28th, week 43 (counting from zero)
#define SUNDAYS_2012_WINTER_TIME_STARTS 43
    { 10, 28 },
    { 11, 4 },
    { 11, 11 },
    { 11, 18 },
    { 11, 25 },
    { 12, 2 },
    { 12, 9 },
    { 12, 16 },
    { 12, 23 },
    { 12, 30 },
    { 0, 0 }
};

static void addMonthly(size_t &index, ClientTestConfig::MultipleLinkedItems_t &subset,
                       const std::string &pre, const std::string &post,
                       const char *suffix, int day, int months)
{
    index++;
    subset.resize(index + 1);
    ClientTestConfig::LinkedItems_t *items = &subset[index];
    items->m_name = std::string("Monthly") + suffix;

    /* month varies */
    std::string parent =
        pre +
        "BEGIN:VEVENT\n"
        "UID:monthly\n"
        "DTSTAMP:20110101T120000Z\n"
        "DTSTART;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T120000\n"
        "DTEND;TZID=Standard Timezone:2012"  "%1$02d" "%2$02d" "T121000\n"
        "SUMMARY:monthly " + suffix + " Berlin\n"
        "RRULE:BYMONTHDAY=1;COUNT=12;FREQ=MONTHLY\n"
        "TRANSP:TRANSPARENT\n"
        "END:VEVENT\n" +
        post;
    std::string child =
        pre +
        "BEGIN:VEVENT\n"
        "UID:monthly\n"
        "DTSTAMP:20110101T120000Z\n"
        "DTSTART;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T120000\n"
        "DTEND;TZID=Standard Timezone:2012"  "%1$02d" "%2$02d" "T121000\n"
        "SUMMARY:%1$04d monthly " + suffix + " Berlin\n"
        "RECURRENCE-ID;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T120000\n"
        "TRANSP:TRANSPARENT\n"
        "END:VEVENT\n" +
        post;
    items->push_back(StringPrintf(parent.c_str(), 1, day));
    for (int month = 1; month <= months; month++) {
        items->push_back(StringPrintf(child.c_str(), month, day));
    }

    if (currentServer() == "exchange") {
        /* month of event varies and UTC time of UNTIL clause (11 during winter time, 10 during summer) */
        std::string single =
            pre +
            "BEGIN:VEVENT\n"
            "SUMMARY:[[activesyncd pseudo event - ignore me]]\n"
            "DTSTART;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T120000\n"
            "DTEND;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T120000\n"
            "RRULE:FREQ=YEARLY;UNTIL=2012" "%1$02d" "%2$02d" "T" "%3$02d" "0000Z;BYMONTHDAY=1;BYMONTH=%1$d\n"
            "UID:monthly\n"
            "TRANSP:TRANSPARENT\n"
            "END:VEVENT\n" +
            post;
        /* first month, last month, UTC time, INTERVAL and sometimes EXDATE varies */
        std::string many =
            pre +
            "BEGIN:VEVENT\n"
            "SUMMARY:[[activesyncd pseudo event - ignore me]]\n"
            "DTSTART;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T120000\n"
            "DTEND;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T120000\n"
            "RRULE:BYMONTHDAY=1;FREQ=MONTHLY;INTERVAL=%5$d;UNTIL=2012" "%3$02d" "%2$02d" "T" "%4$02d" "0000Z\n"
            "%6$s"
            "UID:monthly\n"
            "TRANSP:TRANSPARENT\n"
            "END:VEVENT\n" +
            post;

        auto additionalMonthly = [single, many, day] (int start, int skip, int index, int total) {
            int startMonth = 1 + start - 1;
            std::string event;
            int endMonth = startMonth + index - start;
            int time = (endMonth >= 4 && endMonth <= 10) ? 10 : 11;

            if (start == 0) {
            } else if (start == index) {
                event = StringPrintf(single.c_str(), startMonth, day, time);
            } else {
                // Monthly recurrence uses INTERVAL instead of
                // EXDATEs, in contrast to yearly recurrence
                // (where Exchange somehow didn't grok the
                // INTERVAL). So EXDATEs are only necessary
                // for the first, second, last case.
                if (skip == -1 ) {
                    std::string exdates;
                    for (int month = startMonth; month <= endMonth; month++) {
                        int step = month - startMonth;
                        // a gap?
                        if (step > 1 && step < total - start - 1) {
                            exdates +=
                                StringPrintf("EXDATE;TZID=Standard Timezone:2012%02d01T120000\n",
                                             month);
                        }
                    }
                    event = StringPrintf(many.c_str(), startMonth, day, endMonth, time, 1, exdates.c_str());
                } else {
                    event = StringPrintf(many.c_str(), startMonth, day, endMonth, time, skip + 1, "");
                }
            }

            SE_LOG_DEBUG(NULL, "additional monthly: start %d, skip %d, index %d/%d:\n%s",
                         start, skip, index, total,
                         event.c_str());
            return event;
        };
        items->m_testLinkedItemsSubsetAdditional = additionalMonthly;
    }
}


void ClientTest::getTestData(const char *type, Config &config)
{
    std::string server = currentServer();

    config = Config();
    char *env = getenv("CLIENT_TEST_RETRY");
    config.m_retrySync = (env && !strcmp (env, "t")) ?true :false;
    env = getenv("CLIENT_TEST_RESEND");
    config.m_resendSync = (env && !strcmp (env, "t")) ?true :false;
    env = getenv("CLIENT_TEST_SUSPEND");
    config.m_suspendSync = (env && !strcmp (env, "t")) ?true :false;
    config.m_sourceKnowsItemSemantic = true;
    config.m_linkedItemsRelaxedSemantic = true;
    config.m_itemType = "";
    config.m_import = import;
    config.m_dump = dump;
    config.m_compare = compare;
    // Sync::*::testExtensions not enabled by default.
    config.m_update = 0;
    config.m_genericUpdate = update;

    // redirect requests for "eds_event" towards "eds_event_noutc"?
    bool noutc = false;
    env = getenv ("CLIENT_TEST_NOUTC");
    if (env && !strcmp (env, "t")) {
        noutc = true;
    }

    config.m_mangleItem = mangleGeneric;

    // True for most item kinds, exceptions set below.
    config.m_uniqueID = true;

    static std::set<std::string> vCardEssential = { "FN", "N", "UID", "VERSION" },
        iCalEssential = { "DTSTART", "DTEND", "DTSTAMP", "SUMMARY", "UID", "RRULE", "RECURRENCE-ID", "VERSION" };
    // RRULE is not essential for a valid item, but removing it has implications
    // for other properties (EXDATE) and other items (detached recurrences) and
    // thus cannot be tested in testRemoveProperties (because it doesn't know about
    // these inter-depdendencies).

    if (!strcmp(type, "eds_contact")) {
        config.m_sourceName = "eds_contact";
        config.m_sourceNameServerTemplate = "addressbook";
        config.m_uri = "card3"; // ScheduleWorld
        config.m_type = "text/vcard";
        config.m_essentialProperties = vCardEssential;
        config.m_uniqueID = false;
        config.m_insertItem =
            "BEGIN:VCARD\n"
            "VERSION:3.0\n"
            "TITLE:tester\n"
            "FN:John Doe\n"
            "N:Doe;John;;;\n"
            "UID:25741c35e5431f054444fdf4571219c3\n"
            "TEL;TYPE=WORK;TYPE=VOICE:business 1\n"
            "X-EVOLUTION-FILE-AS:Doe\\, John\n"
            "X-MOZILLA-HTML:FALSE\n"
            "END:VCARD\n";
        config.m_updateItem =
            "BEGIN:VCARD\n"
            "VERSION:3.0\n"
            "TITLE:tester\n"
            "FN:Joan Doe\n"
            "N:Doe;Joan;;;\n"
            "UID:25741c35e5431f054444fdf4571219c3\n"
            "X-EVOLUTION-FILE-AS:Doe\\, Joan\n"
            "TEL;TYPE=WORK;TYPE=VOICE:business 2\n"
            "BDAY:2006-01-08\n"
            "X-MOZILLA-HTML:TRUE\n"
            "END:VCARD\n";
        /* adds a second phone number: */
        config.m_complexUpdateItem =
            "BEGIN:VCARD\n"
            "VERSION:3.0\n"
            "TITLE:tester\n"
            "FN:Joan Doe\n"
            "N:Doe;Joan;;;\n"
            "UID:25741c35e5431f054444fdf4571219c3\n"
            "X-EVOLUTION-FILE-AS:Doe\\, Joan\n"
            "TEL;TYPE=WORK;TYPE=VOICE:business 1\n"
            "TEL;TYPE=HOME;TYPE=VOICE:home 2\n"
            "BDAY:2006-01-08\n"
            "X-MOZILLA-HTML:TRUE\n"
            "END:VCARD\n";
        /* add a telephone number, email and X-AIM to initial item */
        config.m_mergeItem1 =
            "BEGIN:VCARD\n"
            "VERSION:3.0\n"
            "TITLE:tester\n"
            "FN:John Doe\n"
            "N:Doe;John;;;\n"
            "UID:25741c35e5431f054444fdf4571219c3\n"
            "X-EVOLUTION-FILE-AS:Doe\\, John\n"
            "X-MOZILLA-HTML:FALSE\n"
            "TEL;TYPE=WORK;TYPE=VOICE:business 1\n"
            "EMAIL:john.doe@work.com\n"
            "X-AIM:AIM JOHN\n"
            "END:VCARD\n";
        config.m_mergeItem2 =
            "BEGIN:VCARD\n"
            "VERSION:3.0\n"
            "TITLE:developer\n"
            "FN:John Doe\n"
            "N:Doe;John;;;\n"
            "UID:25741c35e5431f054444fdf4571219c3\n"
            "TEL;TYPE=WORK;TYPE=VOICE:123456\n"
            "X-EVOLUTION-FILE-AS:Doe\\, John\n"
            "X-MOZILLA-HTML:TRUE\n"
            "BDAY:2006-01-08\n"
            "END:VCARD\n";
        // use NOTE and N to make the item unique
        config.m_templateItem =
            "BEGIN:VCARD\n"
            "VERSION:3.0\n"
            "TITLE:tester\n"
            "N:Doe;<<UNIQUE>>;<<REVISION>>;;\n"
            "FN:<<UNIQUE>> Doe\n"
            "UID:<<UNIQUE>>-25741c35e5431f054444fdf4571219c3\n"
            "TEL;TYPE=WORK;TYPE=VOICE:business 1\n"
            "X-EVOLUTION-FILE-AS:Doe\\, <<UNIQUE>>\n"
            "X-MOZILLA-HTML:FALSE\n"
            "NOTE:<<REVISION>>\n"
            "END:VCARD\n";  
        config.m_uniqueProperties = "";
        config.m_sizeProperty = "NOTE";
        config.m_testcases = "testcases/eds_contact.vcf";

        if (server == "exchange" ||
            server == "googleeas") {
            // X-MOZILLA-HTML not supported by ActiveSync.
            boost::replace_first(config.m_updateItem, "X-MOZILLA-HTML:TRUE", "X-MOZILLA-HTML:FALSE");
            boost::replace_first(config.m_complexUpdateItem, "X-MOZILLA-HTML:TRUE", "X-MOZILLA-HTML:FALSE");
            boost::replace_first(config.m_mergeItem2, "X-MOZILLA-HTML:TRUE", "X-MOZILLA-HTML:FALSE");
        }
    } else if (!strcmp(type, "eds_event") && !noutc) {
        config.m_sourceName = "eds_event";
        config.m_sourceNameServerTemplate = "calendar";
        config.m_uri = "cal2"; // ScheduleWorld
        config.m_type = "text/x-vcalendar";
        config.m_essentialProperties = iCalEssential;
        if (server == "exchange") {
            // currently cannot remove EXDATE properties, see BMC #24290
            config.m_essentialProperties.insert("EXDATE");
        }
        config.m_mangleItem = mangleICalendar20;
        config.m_insertItem =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VEVENT\n"
            "SUMMARY:phone meeting - old\n"
            "DTEND:20060406T163000Z\n"
            "DTSTART:20060406T160000Z\n"
            "UID:1234567890!@#$%^&*()<>@dummyVEVENT\n"
            "DTSTAMP:20060406T211449Z\n"
            "LAST-MODIFIED:20060409T213201Z\n"
            "CREATED:20060409T213201Z\n"
            "LOCATION:my office\n"
            "DESCRIPTION:let's talk<<REVISION>>\n"
            "CLASS:PUBLIC\n"
            "TRANSP:OPAQUE\n"
            "SEQUENCE:XXX\n"
            "END:VEVENT\n"
            "END:VCALENDAR\n";
        config.m_updateItem =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VEVENT\n"
            "SUMMARY:meeting on site - updated\n"
            "DTEND:20060406T163000Z\n"
            "DTSTART:20060406T160000Z\n"
            "UID:1234567890!@#$%^&*()<>@dummyVEVENT\n"
            "DTSTAMP:20060406T211449Z\n"
            "LAST-MODIFIED:20060409T213201Z\n"
            "CREATED:20060409T213201Z\n"
            "SEQUENCE:XXX\n"
            "LOCATION:big meeting room\n"
            "DESCRIPTION:nice to see you\n"
            "CLASS:PUBLIC\n"
            "TRANSP:OPAQUE\n"
            "END:VEVENT\n"
            "END:VCALENDAR\n";
        /* change location and description of insertItem in testMerge(), add alarm */
        config.m_mergeItem1 =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VEVENT\n"
            "SUMMARY:phone meeting\n"
            "DTEND:20060406T163000Z\n"
            "DTSTART:20060406T160000Z\n"
            "UID:1234567890!@#$%^&*()<>@dummyVEVENT\n"
            "DTSTAMP:20060406T211449Z\n"
            "LAST-MODIFIED:20060409T213201Z\n"
            "CREATED:20060409T213201Z\n"
            "SEQUENCE:XXX\n"
            "LOCATION:calling from home\n"
            "DESCRIPTION:let's talk\n"
            "CLASS:PUBLIC\n"
            "TRANSP:OPAQUE\n"
            "BEGIN:VALARM\n"
            "DESCRIPTION:alarm\n"
            "ACTION:DISPLAY\n"
            "TRIGGER;VALUE=DURATION;RELATED=START:-PT15M\n"
            "END:VALARM\n"
            "END:VEVENT\n"
            "END:VCALENDAR\n";
        /* change location to something else, add category */
        config.m_mergeItem2 =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VEVENT\n"
            "SUMMARY:phone meeting\n"
            "DTEND:20060406T163000Z\n"
            "DTSTART:20060406T160000Z\n"
            "UID:1234567890!@#$%^&*()<>@dummyVEVENT\n"
            "DTSTAMP:20060406T211449Z\n"
            "LAST-MODIFIED:20060409T213201Z\n"
            "CREATED:20060409T213201Z\n"
            "SEQUENCE:XXX\n"
            "LOCATION:my office\n"
            "CATEGORIES:WORK\n"
            "DESCRIPTION:what the heck\\, let's even shout a bit\n"
            "CLASS:PUBLIC\n"
            "TRANSP:OPAQUE\n"
            "END:VEVENT\n"
            "END:VCALENDAR\n";

        // Servers have very different understandings of how
	// recurrence interacts with time zones and RRULE.
	// Must use different test cases for some servers to
	// avoid having the linkedItems test cases fail
	// because of that.
	// default: time zones + UNTIL in UTC, with VALARM
        config.m_linkedItems.resize(1);
        config.m_linkedItems[0].m_name = "Default";
        config.m_linkedItems[0].resize(2);
        config.m_linkedItems[0][0] =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VTIMEZONE\n"
            "TZID:Europe/Berlin\n"
            "X-LIC-LOCATION:Europe/Berlin\n"
            "BEGIN:DAYLIGHT\n"
            "TZOFFSETFROM:+0100\n"
            "TZOFFSETTO:+0200\n"
            "TZNAME:CEST\n"
            "DTSTART:19700329T020000\n"
            "RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3\n"
            "END:DAYLIGHT\n"
            "BEGIN:STANDARD\n"
            "TZOFFSETFROM:+0200\n"
            "TZOFFSETTO:+0100\n"
            "TZNAME:CET\n"
            "DTSTART:19701025T030000\n"
            "RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10\n"
            "END:STANDARD\n"
            "END:VTIMEZONE\n"
            "BEGIN:VEVENT\n"
            "UID:20080407T193125Z-19554-727-1-50@dummyVEVENT\n"
            "DTSTAMP:20080407T193125Z\n"
            "DTSTART;TZID=Europe/Berlin:20080406T090000\n"
            "DTEND;TZID=Europe/Berlin:20080406T093000\n"
            "TRANSP:OPAQUE\n"
            "SEQUENCE:XXX\n"
            "SUMMARY:Recurring\n"
            "DESCRIPTION:recurs each Monday\\, 10 times\n"
            "CLASS:PUBLIC\n"
            "RRULE:FREQ=WEEKLY;UNTIL=20080608T070000Z;INTERVAL=1;BYDAY=SU\n"
            "CREATED:20080407T193241Z\n"
            "LAST-MODIFIED:20080407T193241Z\n"
            "END:VEVENT\n"
            "END:VCALENDAR\n";
        config.m_linkedItems[0][1] =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VTIMEZONE\n"
            "TZID:Europe/Berlin\n"
            "X-LIC-LOCATION:Europe/Berlin\n"
            "BEGIN:DAYLIGHT\n"
            "TZOFFSETFROM:+0100\n"
            "TZOFFSETTO:+0200\n"
            "TZNAME:CEST\n"
            "DTSTART:19700329T020000\n"
            "RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3\n"
            "END:DAYLIGHT\n"
            "BEGIN:STANDARD\n"
            "TZOFFSETFROM:+0200\n"
            "TZOFFSETTO:+0100\n"
            "TZNAME:CET\n"
            "DTSTART:19701025T030000\n"
            "RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10\n"
            "END:STANDARD\n"
            "END:VTIMEZONE\n"
            "BEGIN:VEVENT\n"
            "UID:20080407T193125Z-19554-727-1-50@dummyVEVENT\n"
            "DTSTAMP:20080407T193125Z\n"
            "DTSTART;TZID=Europe/Berlin:20080413T090000\n"
            "DTEND;TZID=Europe/Berlin:20080413T093000\n"
            "TRANSP:OPAQUE\n"
            "SEQUENCE:XXX\n"
            "SUMMARY:Recurring: Modified\n"
            "CLASS:PUBLIC\n"
            "CREATED:20080407T193241Z\n"
            "LAST-MODIFIED:20080407T193647Z\n"
            "RECURRENCE-ID;TZID=Europe/Berlin:20080413T090000\n"
            "DESCRIPTION:second instance modified\n"
            "END:VEVENT\n"
            "END:VCALENDAR\n";

        bool recurringAllDay = false;
        bool recurringNoTZ = false;
        bool subsets = false;

	if (server == "funambol") {
	    // converts UNTIL into floating time - broken?!
            config.m_linkedItems[0].m_name = "UntilFloatTime";
	    config.m_linkedItems[0][0] =
	        "BEGIN:VCALENDAR\n"
                "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
                "VERSION:2.0\n"
                "BEGIN:VTIMEZONE\n"
                "TZID:Europe/Berlin\n"
                "X-LIC-LOCATION:Europe/Berlin\n"
                "BEGIN:DAYLIGHT\n"
                "TZOFFSETFROM:+0100\n"
                "TZOFFSETTO:+0200\n"
                "TZNAME:CEST\n"
                "DTSTART:19700329T020000\n"
                "RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3\n"
                "END:DAYLIGHT\n"
                "BEGIN:STANDARD\n"
                "TZOFFSETFROM:+0200\n"
                "TZOFFSETTO:+0100\n"
                "TZNAME:CET\n"
                "DTSTART:19701025T030000\n"
                "RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10\n"
                "END:STANDARD\n"
                "END:VTIMEZONE\n"
                "BEGIN:VEVENT\n"
                "UID:20080407T193125Z-19554-727-1-50@dummyVEVENT\n"
                "DTSTAMP:20080407T193125Z\n"
                "DTSTART;TZID=Europe/Berlin:20080406T090000\n"
                "DTEND;TZID=Europe/Berlin:20080406T093000\n"
                "TRANSP:OPAQUE\n"
                "SEQUENCE:XXX\n"
                "SUMMARY:Recurring\n"
                "DESCRIPTION:recurs each Monday\\, 10 times\n"
                "CLASS:PUBLIC\n"
                "RRULE:FREQ=WEEKLY;UNTIL=20080608T090000;INTERVAL=1;BYDAY=SU\n"
                "CREATED:20080407T193241Z\n"
                "LAST-MODIFIED:20080407T193241Z\n"
                "END:VEVENT\n"
                "END:VCALENDAR\n";
        } else if (server == "mobical") {
	    // UTC time
            config.m_linkedItems[0].m_name = "UTC";
	    config.m_linkedItems[0][0] =
	        "BEGIN:VCALENDAR\n"
                "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
                "VERSION:2.0\n"
                "BEGIN:VEVENT\n"
                "UID:20080407T193125Z-19554-727-1-50@dummyVEVENT\n"
                "DTSTAMP:20080407T193125Z\n"
                "DTSTART:20080406T070000Z\n"
                "DTEND:20080406T073000Z\n"
                "TRANSP:OPAQUE\n"
                "SEQUENCE:XXX\n"
                "SUMMARY:Recurring\n"
                "DESCRIPTION:recurs each Monday\\, 10 times\n"
                "CLASS:PUBLIC\n"
                "RRULE:FREQ=WEEKLY;UNTIL=20080608T070000Z;INTERVAL=1;BYDAY=SU\n"
                "CREATED:20080407T193241Z\n"
                "LAST-MODIFIED:20080407T193241Z\n"
                "END:VEVENT\n"
                "END:VCALENDAR\n";
            config.m_linkedItems[0][1] =
                "BEGIN:VCALENDAR\n"
                "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
                "VERSION:2.0\n"
                "BEGIN:VEVENT\n"
                "UID:20080407T193125Z-19554-727-1-50@dummyVEVENT\n"
                "DTSTAMP:20080407T193125Z\n"
                "DTSTART:20080413T070000Z\n"
                "DTEND:20080413T073000Z\n"
                "TRANSP:OPAQUE\n"
                "SEQUENCE:XXX\n"
                "SUMMARY:Recurring: Modified\n"
                "CLASS:PUBLIC\n"
                "CREATED:20080407T193241Z\n"
                "LAST-MODIFIED:20080407T193647Z\n"
                "RECURRENCE-ID:20080413T070000Z\n"
                "DESCRIPTION:second instance modified\n"
                "END:VEVENT\n"
                "END:VCALENDAR\n";
        } else if (server == "memotoo") {
	    // local floating time, always, regardless what the original
            // time zone might have been (TZID, UTC, floating)
            config.m_linkedItems[0].m_name = "LocalTime";
	    config.m_linkedItems[0][0] =
	        "BEGIN:VCALENDAR\n"
                "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
                "VERSION:2.0\n"
                "BEGIN:VEVENT\n"
                "UID:20080407T193125Z-19554-727-1-50@dummyVEVENT\n"
                "DTSTAMP:20080407T193125Z\n"
                "DTSTART:20080406T070000\n"
                "DTEND:20080406T073000\n"
                "TRANSP:OPAQUE\n"
                "SEQUENCE:XXX\n"
                "SUMMARY:Recurring\n"
                "DESCRIPTION:recurs each Monday\\, 10 times\n"
                "CLASS:PUBLIC\n"
                "RRULE:FREQ=WEEKLY;UNTIL=20080608T070000;INTERVAL=1;BYDAY=SU\n"
                "CREATED:20080407T193241Z\n"
                "LAST-MODIFIED:20080407T193241Z\n"
                "END:VEVENT\n"
                "END:VCALENDAR\n";
            config.m_linkedItems[0][1] =
                "BEGIN:VCALENDAR\n"
                "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
                "VERSION:2.0\n"
                "BEGIN:VEVENT\n"
                "UID:20080407T193125Z-19554-727-1-50@dummyVEVENT\n"
                "DTSTAMP:20080407T193125Z\n"
                "DTSTART:20080413T070000\n"
                "DTEND:20080413T073000\n"
                "TRANSP:OPAQUE\n"
                "SEQUENCE:XXX\n"
                "SUMMARY:Recurring: Modified\n"
                "CLASS:PUBLIC\n"
                "CREATED:20080407T193241Z\n"
                "LAST-MODIFIED:20080407T193647Z\n"
                "RECURRENCE-ID:20080413T070000\n"
                "DESCRIPTION:second instance modified\n"
                "END:VEVENT\n"
                "END:VCALENDAR\n";

            // also affects normal test items
            for (auto item: {
                    &config.m_insertItem,
                    &config.m_updateItem,
                    &config.m_mergeItem1,
                    &config.m_mergeItem2 }) {
                static const std::regex times(R"del(\b(DTSTART|DTEND)(.*)Z\b)del");
                *item = std::regex_replace(*item, times, "$1$2");
            }
        } else if (server == "exchange") {
            config.m_linkedItems[0].m_name = "StandardTZ";
            for (std::string &item: config.m_linkedItems[0]) {
                // time zone name changes on server to "Standard Timezone",
                // with some information stripped
                boost::replace_all(item,
                                   "Europe/Berlin",
                                   "Standard Timezone");
                // some properties are not stored/supported
                boost::replace_all(item, "TZNAME:CET\n", "");
                boost::replace_all(item, "TZNAME:CEST\n", "");
                boost::replace_all(item, "X-LIC-LOCATION:Standard Timezone\n", "");
            }

            recurringAllDay = true;
            subsets = true;
        } else {
            // in particular for Google Calendar: also try with
            // VALARM, because testing showed that the server works
            // differently with and without VALARM data included
            config.m_linkedItems.resize(2);
            config.m_linkedItems[1].m_name = "WithVALARM";
            config.m_linkedItems[1].resize(2);
            const std::string valarm =
                "BEGIN:VALARM\n"
                "ACTION:DISPLAY\n"
                "DESCRIPTION:This is an event reminder\n"
                "TRIGGER;VALUE=DURATION;RELATED=START:-PT1H\n"
                "X-EVOLUTION-ALARM-UID:foo@bar\n"
                "END:VALARM\nEND:VEVENT";
            config.m_linkedItems[1][0] = config.m_linkedItems[0][0];
            boost::replace_first(config.m_linkedItems[1][0], "END:VEVENT", valarm);
            config.m_linkedItems[1][1] = config.m_linkedItems[0][1];
            boost::replace_first(config.m_linkedItems[1][1], "END:VEVENT", valarm);

            // also enable other linked item variants
            recurringAllDay = true;
            recurringNoTZ = true;
        }

        if (boost::starts_with(server, "google")) {
            // converts local time into time zone of the user,
            // which breaks the test
            recurringNoTZ = false;
        }

        // test is fairly slow, only test with some CalDAV servers
        if (boost::starts_with(server, "apple")) {
            subsets = true;
        }

        if (recurringAllDay) {
            // also test recurring all-day events with exceptions
            size_t index = config.m_linkedItems.size();
            config.m_linkedItems.resize(index + 1);
            config.m_linkedItems[index].m_name = "AllDay";
            config.m_linkedItems[index].resize(2);
            config.m_linkedItems[index][0] =
                "BEGIN:VCALENDAR\n"
                "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
                "VERSION:2.0\n"
                "BEGIN:VEVENT\n"
                "UID:20110829T130000Z-19554-727-1-50@dummyVEVENT\n"
                "DTSTAMP:20080407T193125Z\n"
                "DTSTART;VALUE=DATE:20080406\n"
                "DTEND;VALUE=DATE:20080407\n"
                "TRANSP:OPAQUE\n"
                "SEQUENCE:XXX\n"
                "SUMMARY:Recurring all day event\n"
                "DESCRIPTION:recurs each Monday\\, 3 times\n"
                "CLASS:PUBLIC\n"
                "RRULE:FREQ=WEEKLY;UNTIL=20080420;INTERVAL=1;BYDAY=SU\n"
                "CREATED:20080407T193241Z\n"
                "LAST-MODIFIED:20080407T193241Z\n"
                "END:VEVENT\n"
                "END:VCALENDAR\n";

            // workaround for http://code.google.com/p/google-caldav-issues/issues/detail?id=63
            // Google CalDAV inserts a time into the UNTIL clause, do the same in the
            // reference data.
            if (boost::starts_with(server, "google")) {
                config.m_linkedItems[index].m_name = "AllDayGoogle";
                boost::replace_first(config.m_linkedItems[index][0],
                                     "UNTIL=20080420",
                                     "UNTIL=20080420T070000Z");
            }

            config.m_linkedItems[index][1] =
                "BEGIN:VCALENDAR\n"
                "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
                "VERSION:2.0\n"
                "BEGIN:VEVENT\n"
                "UID:20110829T130000Z-19554-727-1-50@dummyVEVENT\n"
                "DTSTAMP:20080407T193125Z\n"
                "DTSTART;VALUE=DATE:20080413\n"
                "DTEND;VALUE=DATE:20080414\n"
                "TRANSP:OPAQUE\n"
                "SEQUENCE:XXX\n"
                "SUMMARY:Recurring: Modified second instance\n"
                "CLASS:PUBLIC\n"
                "CREATED:20080407T193241Z\n"
                "LAST-MODIFIED:20080407T193647Z\n"
                "RECURRENCE-ID;VALUE=DATE:20080413\n"
                "DESCRIPTION:second instance modified\n"
                "END:VEVENT\n"
                "END:VCALENDAR\n";
	}

        if (recurringNoTZ) {
            // also test recurring event with no timezone
            size_t index = config.m_linkedItems.size();
            config.m_linkedItems.resize(index + 1);
            config.m_linkedItems[index].m_name = "NoTZ";
            config.m_linkedItems[index].resize(2);
            config.m_linkedItems[index][0] = config.m_linkedItems[0][0];
            config.m_linkedItems[index][1] = config.m_linkedItems[0][1];
            stripComponent(config.m_linkedItems[index][0], "VTIMEZONE");
            stripParameters(config.m_linkedItems[index][0], "TZID");
            stripComponent(config.m_linkedItems[index][1], "VTIMEZONE");
            stripParameters(config.m_linkedItems[index][1], "TZID");
        }

        if (subsets) {
            static const std::string pre =
                "BEGIN:VCALENDAR\n"
                "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
                "VERSION:2.0\n"
                "BEGIN:VTIMEZONE\n"
                // Actually, this is Europe/Berlin.
                // Was renamed to fit the simplified activesyncd naming
                // and DTSTART was adapted.
                "TZID:Standard Timezone\n"
                "BEGIN:STANDARD\n"
                "DTSTART:19701025T030000\n"
                "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\n"
                "TZOFFSETFROM:+0200\n"
                "TZOFFSETTO:+0100\n"
                "END:STANDARD\n"
                "BEGIN:DAYLIGHT\n"
                "DTSTART:19700329T020000\n"
                "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\n"
                "TZOFFSETFROM:+0100\n"
                "TZOFFSETTO:+0200\n"
                "END:DAYLIGHT\n"
                "END:VTIMEZONE\n";
            static const std::string post =
                "END:VCALENDAR\n";

            size_t index = config.m_linkedItemsSubset.size();
            config.m_linkedItemsSubset.resize(index + 1);
            ClientTestConfig::LinkedItems_t *items = &config.m_linkedItemsSubset[index];
            items->m_name = "Yearly";
            /* year varies */
            std::string parent =
                pre +
                "BEGIN:VEVENT\n"
                "UID:yearly\n"
                "DTSTAMP:20110101T120000Z\n"
                "DTSTART;TZID=Standard Timezone:" "%1$04d" "0101T120000\n"
                "DTEND;TZID=Standard Timezone:"  "%1$04d" "0101T121000\n"
                "SUMMARY:yearly Berlin\n"
                "RRULE:BYMONTH=1;BYMONTHDAY=1;UNTIL=20140101T110000Z;FREQ=YEARLY\n"
                "TRANSP:TRANSPARENT\n"
                "END:VEVENT\n" +
                post;
            std::string child =
                pre +
                "BEGIN:VEVENT\n"
                "UID:yearly\n"
                "DTSTAMP:20110101T120000Z\n"
                "DTSTART;TZID=Standard Timezone:" "%1$04d" "0101T120000\n"
                "DTEND;TZID=Standard Timezone:"  "%1$04d" "0101T121000\n"
                "SUMMARY:" "%1$04d" "yearly Berlin\n"
                "RECURRENCE-ID;TZID=Standard Timezone:" "%1$04d" "0101T120000\n"
                "TRANSP:TRANSPARENT\n"
                "END:VEVENT\n" +
                post;
            auto &vector = config.m_linkedItemsSubset[index];
            vector.emplace_back(StringPrintf(parent.c_str(), 2012));
            vector.emplace_back(StringPrintf(child.c_str(), 2012));
            vector.emplace_back(StringPrintf(child.c_str(), 2013));
            vector.emplace_back(StringPrintf(child.c_str(), 2014));
            if (server == "exchange") {
                /* only year of event varies */
                std::string single =
                    pre +
                    "BEGIN:VEVENT\n"
                    "SUMMARY:[[activesyncd pseudo event - ignore me]]\n"
                    "DTSTART;TZID=Standard Timezone:" "%1$04d" "0101T120000\n"
                    "DTEND;TZID=Standard Timezone:" "%1$04d" "0101T120000\n"
                    "RRULE:FREQ=YEARLY;UNTIL=" "%1$04d" "0101T110000Z;BYMONTHDAY=1;BYMONTH=1\n"
                    "UID:yearly\n"
                    "TRANSP:TRANSPARENT\n"
                    "END:VEVENT\n" +
                    post;
                /* first year, last year and EXDATE varies */
                std::string many =
                    pre +
                    "BEGIN:VEVENT\n"
                    "SUMMARY:[[activesyncd pseudo event - ignore me]]\n"
                    "DTSTART;TZID=Standard Timezone:" "%1$04d" "0101T120000\n"
                    "DTEND;TZID=Standard Timezone:" "%1$04d" "0101T120000\n"
                    "RRULE:FREQ=YEARLY;UNTIL=" "%2$04d" "0101T110000Z;BYMONTHDAY=1;BYMONTH=1\n"
                    "%3$s"
                    "UID:yearly\n"
                    "TRANSP:TRANSPARENT\n"
                    "END:VEVENT\n" +
                    post;

                auto additionalYearly = [single, many] (int start, int skip, int index, int total) {
                    int startYear = 2012 + start - 1;
                    std::string event;

                    if (start == 0) {
                        // no missing parent, nothing to add
                    } else if (start == index) {
                        // inserting a single detached recurrence
                        event = StringPrintf(single.c_str(), startYear);
                    } else {
                        // many detached recurrences
                        int endYear = startYear + index - start;
                        std::string exdates;
                        for (int year = startYear; year <= endYear; year++) {
                            // a gap?
                            if ((year - startYear) % (skip + 1)) {
                                exdates +=
                                    StringPrintf("EXDATE;TZID=Standard Timezone:%04d0101T120000\n",
                                                 year);
                            }
                        }
                        event = StringPrintf(many.c_str(), startYear, endYear, exdates.c_str());
                    }


                    SE_LOG_DEBUG(NULL, "additional yearly: start %d, skip %d, index %d/%d:\n%s",
                                 start, skip, index, total,
                                 event.c_str());
                    return event;
                };
                items->m_testLinkedItemsSubsetAdditional = additionalYearly;
            }

            addMonthly(index, config.m_linkedItemsSubset, pre, post, "First", 1, 12);
            addMonthly(index, config.m_linkedItemsSubset, pre, post, "Middle", 1, 6);

            index++;
            config.m_linkedItemsSubset.resize(index + 1);
            items = &config.m_linkedItemsSubset[index];
            items->m_name = "Weekly";
            items->push_back(pre +
                             "BEGIN:VEVENT\n"
                             "UID:weekly\n"
                             "DTSTAMP:20110101T120000Z\n"
                             "DTSTART;TZID=Standard Timezone:20120101T140000\n"
                             "DTEND;TZID=Standard Timezone:20120101T141000\n"
                             "SUMMARY:weekly Sunday Berlin\n"
                             "RRULE:FREQ=WEEKLY;COUNT=54;BYDAY=SU\n"
                             "TRANSP:TRANSPARENT\n"
                             "END:VEVENT\n" +
                             post);
            for (int i = 0; sundays[i].m_month; i++) {
                items->push_back(StringPrintf((pre +
                                               "BEGIN:VEVENT\n"
                                               "UID:weekly\n"
                                               "DTSTAMP:20110101T120000Z\n"
                                               "DTSTART;TZID=Standard Timezone:2012" "%1$02d%2$02d" "T140000\n"
                                               "DTEND;TZID=Standard Timezone:2012" "%1$02d%2$02d" "T141000\n"
                                               "SUMMARY:2012-%1$02d-%2$02d %3$d. weekly Sunday Berlin\n"
                                               "RECURRENCE-ID;TZID=Standard Timezone:2012" "%1$02d%2$02d" "T140000\n"
                                               "TRANSP:TRANSPARENT\n"
                                               "END:VEVENT\n" +
                                               post).c_str(),
                                              sundays[i].m_month,
                                              sundays[i].m_day,
                                              i + 1));
            }

            if (server == "exchange") {
                /* date varies and UTC time of UNTIL clause (11 during winter time, 10 during summer) */
                std::string single =
                    pre +
                    "BEGIN:VEVENT\n"
                    "SUMMARY:[[activesyncd pseudo event - ignore me]]\n"
                    "DTSTART;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T140000\n"
                    "DTEND;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T140000\n"
                    "RRULE:FREQ=YEARLY;UNTIL=2012" "%1$02d" "%2$02d" "T" "%3$02d" "0000Z;BYMONTHDAY=%2$d;BYMONTH=%1$d\n"
                    "UID:weekly\n"
                    "TRANSP:TRANSPARENT\n"
                    "END:VEVENT\n" +
                    post;
                /* first month, last month, UTC time, INTERVAL and sometimes EXDATE varies */
                std::string many =
                    pre +
                    "BEGIN:VEVENT\n"
                    "SUMMARY:[[activesyncd pseudo event - ignore me]]\n"
                    "DTSTART;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T140000\n"
                    "DTEND;TZID=Standard Timezone:2012" "%1$02d" "%2$02d" "T140000\n"
                    "RRULE:BYDAY=SU;FREQ=WEEKLY;INTERVAL=%6$d;UNTIL=2012" "%3$02d" "%4$02d" "T" "%5$02d" "0000Z\n"
                    "%7$s"
                    "UID:weekly\n"
                    "TRANSP:TRANSPARENT\n"
                    "END:VEVENT\n" +
                    post;

                auto additionalWeekly = [single, many] (int start, int skip, int index, int total) {
                    int startWeek = start - 1; // numbered from zero in "sundays" array
                    if (startWeek < 0) {
                        startWeek = 0;
                    }
                    std::string event;
                    int endWeek = startWeek + index - start;
                    int time = (endWeek >= SUNDAYS_2012_WINTER_TIME_ENDS &&
                                endWeek < SUNDAYS_2012_WINTER_TIME_STARTS) ? 12 : 13;
                    int startMonth = sundays[startWeek].m_month;
                    int startDay = sundays[startWeek].m_day;

                    if (start == 0) {
                    } else if (start == index) {
                        event = StringPrintf(single.c_str(), startMonth, startDay, time);
                    } else {
                        int endMonth = sundays[endWeek].m_month;
                        int endDay = sundays[endWeek].m_day;

                        // Weekly recurrence uses INTERVAL instead of
                        // EXDATEs, in contrast to yearly recurrence
                        // (where Exchange somehow didn't grok the
                        // INTERVAL). So EXDATEs are only necessary
                        // for the first, second, last case.
                        std::string exdates;
                        if (skip == -1 ) {
                            for (int week = startWeek; week <= endWeek; week++) {
                                int step = week - startWeek;
                                // a gap?
                                if (step > 1 && step < total - start - 1) {
                                    exdates +=
                                        StringPrintf("EXDATE;TZID=Standard Timezone:2012%02d%02dT140000\n",
                                                     sundays[week].m_month,
                                                     sundays[week].m_day);
                                }
                            }
                            event = StringPrintf(many.c_str(),
                                                 startMonth, startDay,
                                                 endMonth, endDay,
                                                 time, 1, exdates.c_str());
                        } else {
                            event = StringPrintf(many.c_str(),
                                                 startMonth, startDay,
                                                 endMonth, endDay,
                                                 time, skip + 1, "");
                        }
                    }

                    SE_LOG_DEBUG(NULL, "additional weekly: start %d, skip %d, index %d/%d:\n%s",
                                 start, skip, index, total,
                                 event.c_str());
                    return event;
                };

                items ->m_testLinkedItemsSubsetAdditional = additionalWeekly;
            }
        }

        config.m_templateItem = config.m_insertItem;
        config.m_uniqueProperties = "SUMMARY:UID:LOCATION";
        config.m_sizeProperty = "DESCRIPTION";
        config.m_testcases = "testcases/eds_event.ics";
    } else if (!strcmp(type, "eds_event_noutc") ||
               (!strcmp(type, "eds_event") && noutc)) {
        config.m_sourceName = "eds_event";
        config.m_sourceNameServerTemplate = "calendar";
        config.m_uri = "cal2"; // ScheduleWorld
        config.m_type = "text/x-vcalendar";
        config.m_essentialProperties = iCalEssential;
        config.m_mangleItem = mangleICalendar20;
        config.m_insertItem =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VTIMEZONE\n"
            "TZID:Asia/Shanghai\n"
            "BEGIN:STANDARD\n"
            "DTSTART:19670101T000000\n"
            "TZOFFSETFROM:+0800\n"
            "TZOFFSETTO:+0800\n"
            "END:STANDARD\n"
            "END:VTIMEZONE\n"
            "BEGIN:VTIMEZONE\n"
            "TZID:/freeassociation.sourceforge.net/Tzfile/Asia/Shanghai\n"
            "X-LIC-LOCATION:Asia/Shanghai\n"
            "BEGIN:STANDARD\n"
            "TZNAME:CST\n"
            "DTSTART:19700914T230000\n"
            "TZOFFSETFROM:+0800\n"
            "TZOFFSETTO:+0800\n"
            "END:STANDARD\n"
            "END:VTIMEZONE\n"
            "BEGIN:VEVENT\n"
            "SUMMARY:phone meeting\n"
            "DTEND;TZID=/freeassociation.sourceforge.net/Tzfile/Asia/Shanghai:20060406T163000\n"
            "DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/Asia/Shanghai:20060406T160000\n"
            "UID:1234567890!@#$%^&*()<>@dummyVEVENT\n"
            "DTSTAMP:20060406T211449Z\n"
            "LAST-MODIFIED:20060409T213201Z\n"
            "CREATED:20060409T213201Z\n"
            "LOCATION:my office\n"
            "DESCRIPTION:let's talk<<REVISION>>\n"
            "CLASS:PUBLIC\n"
            "TRANSP:OPAQUE\n"
            "SEQUENCE:XXX\n"
            "END:VEVENT\n"
            "END:VCALENDAR\n";
        config.m_updateItem =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VTIMEZONE\n"
            "TZID:Asia/Shanghai\n"
            "BEGIN:STANDARD\n"
            "DTSTART:19670101T000000\n"
            "TZOFFSETFROM:+0800\n"
            "TZOFFSETTO:+0800\n"
            "END:STANDARD\n"
            "END:VTIMEZONE\n"
            "BEGIN:VTIMEZONE\n"
            "TZID:/freeassociation.sourceforge.net/Tzfile/Asia/Shanghai\n"
            "X-LIC-LOCATION:Asia/Shanghai\n"
            "BEGIN:STANDARD\n"
            "TZNAME:CST\n"
            "DTSTART:19700914T230000\n"
            "TZOFFSETFROM:+0800\n"
            "TZOFFSETTO:+0800\n"
            "END:STANDARD\n"
            "END:VTIMEZONE\n"
            "BEGIN:VEVENT\n"
            "SUMMARY:meeting on site\n"
            "DTEND;TZID=/freeassociation.sourceforge.net/Tzfile/Asia/Shanghai:20060406T163000\n"
            "DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/Asia/Shanghai:20060406T160000\n"
            "UID:1234567890!@#$%^&*()<>@dummyVEVENT\n"
            "DTSTAMP:20060406T211449Z\n"
            "LAST-MODIFIED:20060409T213201Z\n"
            "CREATED:20060409T213201Z\n"
            "LOCATION:big meeting room\n"
            "DESCRIPTION:nice to see you\n"
            "CLASS:PUBLIC\n"
            "TRANSP:OPAQUE\n"
            "SEQUENCE:XXX\n"
            "END:VEVENT\n"
            "END:VCALENDAR\n";
        /* change location and description of insertItem in testMerge(), add alarm */
        config.m_mergeItem1 = "";
        config.m_mergeItem2 = "";
        config.m_templateItem = config.m_insertItem;
        config.m_uniqueProperties = "SUMMARY:UID:LOCATION";
        config.m_sizeProperty = "DESCRIPTION";
        config.m_testcases = "testcases/eds_event.ics";
    } else if(!strcmp(type, "eds_task")) {
        config.m_sourceName = "eds_task";
        config.m_sourceNameServerTemplate = "todo";
        config.m_uri = "task2"; // ScheduleWorld
        config.m_type = "text/x-vcalendar";
        config.m_essentialProperties = iCalEssential;
        config.m_mangleItem = mangleICalendar20;
        config.m_insertItem =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VTODO\n"
            "UID:20060417T173712Z-4360-727-1-2730@dummyVTODO\n"
            "DTSTAMP:20060417T173712Z\n"
            "SUMMARY:do me\n"
            "DESCRIPTION:to be done<<REVISION>>\n"
            "PRIORITY:0\n"
            "STATUS:NEEDS-ACTION\n"
            "CREATED:20060417T173712Z\n"
            "LAST-MODIFIED:20060417T173712Z\n"
            "END:VTODO\n"
            "END:VCALENDAR\n";
        config.m_updateItem =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VTODO\n"
            "UID:20060417T173712Z-4360-727-1-2730@dummyVTODO\n"
            "DTSTAMP:20060417T173712Z\n"
            "SUMMARY:do me ASAP\n"
            "DESCRIPTION:to be done\n"
            "PRIORITY:1\n"
            "STATUS:NEEDS-ACTION\n"
            "CREATED:20060417T173712Z\n"
            "LAST-MODIFIED:20060417T173712Z\n"
            "END:VTODO\n"
            "END:VCALENDAR\n";
        /* change summary in insertItem in testMerge() */
        config.m_mergeItem1 =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VTODO\n"
            "UID:20060417T173712Z-4360-727-1-2730@dummyVTODO\n"
            "DTSTAMP:20060417T173712Z\n"
            "SUMMARY:do me please\\, please\n"
            "DESCRIPTION:to be done\n"
            "PRIORITY:0\n"
            "STATUS:NEEDS-ACTION\n"
            "CREATED:20060417T173712Z\n"
            "LAST-MODIFIED:20060417T173712Z\n"
            "END:VTODO\n"
            "END:VCALENDAR\n";
        config.m_mergeItem2 =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VTODO\n"
            "UID:20060417T173712Z-4360-727-1-2730@dummyVTODO\n"
            "DTSTAMP:20060417T173712Z\n"
            "SUMMARY:do me\n"
            "DESCRIPTION:to be done\n"
            "PRIORITY:7\n"
            "STATUS:NEEDS-ACTION\n"
            "CREATED:20060417T173712Z\n"
            "LAST-MODIFIED:20060417T173712Z\n"
            "END:VTODO\n"
            "END:VCALENDAR\n";
        config.m_templateItem = config.m_insertItem;
        config.m_uniqueProperties = "SUMMARY:UID";
        config.m_sizeProperty = "DESCRIPTION";
        config.m_testcases = "testcases/eds_task.ics";
    } else if(!strcmp(type, "eds_memo")) {
        // The "eds_memo" test uses iCalendar 2.0 VJOURNAL
        // as format because synccompare doesn't handle
        // plain text. A backend which wants to use this
        // test data must support importing/exporting
        // the test data in that format, see EvolutionMemoSource
        // for an example.
        config.m_uri = "note"; // ScheduleWorld
        config.m_sourceName = "eds_memo";
        config.m_sourceNameServerTemplate = "memo";
        config.m_type = "memo";
        config.m_itemType = "text/calendar";
        config.m_essentialProperties = iCalEssential;
        config.m_mangleItem = mangleICalendar20;

        // Although iCalendar 2.0 is used in EDS, uniqueness is not
        // really enforced when syncing. The test data does not have
        // UID set and thus would not pass testInsertTwice.
        config.m_uniqueID = false;

        config.m_insertItem =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VJOURNAL\n"
            "SUMMARY:Summary\n"
            "DESCRIPTION:Summary\\nBody text\n"
            "END:VJOURNAL\n"
            "END:VCALENDAR\n";
        config.m_updateItem =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VJOURNAL\n"
            "SUMMARY:Summary Modified\n"
            "DESCRIPTION:Summary Modified\\nBody text\n"
            "END:VJOURNAL\n"
            "END:VCALENDAR\n";
        /* change summary, as in updateItem, and the body in the other merge item */
        config.m_mergeItem1 = config.m_updateItem;
        config.m_mergeItem2 =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VJOURNAL\n"
            "SUMMARY:Summary\n"
            "DESCRIPTION:Summary\\nBody modified\n"
            "END:VJOURNAL\n"
            "END:VCALENDAR\n";                
        config.m_templateItem =
            "BEGIN:VCALENDAR\n"
            "PRODID:-//Ximian//NONSGML Evolution Calendar//EN\n"
            "VERSION:2.0\n"
            "BEGIN:VJOURNAL\n"
            "SUMMARY:Summary\n"
            "DESCRIPTION:Summary\\nBody text <<REVISION>>\n"
            "END:VJOURNAL\n"
            "END:VCALENDAR\n";
        config.m_uniqueProperties = "SUMMARY:DESCRIPTION";
        config.m_sizeProperty = "DESCRIPTION";
        config.m_testcases = "testcases/eds_memo.ics";
    }else if (!strcmp (type, "calendar+todo")) {
        config.m_uri="";
        config.m_sourceNameServerTemplate = "calendar+todo";
    }
}

void CheckSyncReport::check(SyncMLStatus status, SyncReport &report) const
{
    if (m_report) {
        *m_report = report;
    }

    stringstream str;

    str << report;
    str << "----------|--------CLIENT---------|--------SERVER---------|\n";
    str << "          |  NEW  |  MOD  |  DEL  |  NEW  |  MOD  |  DEL  |\n";
    str << "----------|-----------------------------------------------|\n";
    str << StringPrintf("Expected  |  %3d  |  %3d  |  %3d  |  %3d  |  %3d  |  %3d  |\n",
                        clientAdded, clientUpdated, clientDeleted,
                        serverAdded, serverUpdated, serverDeleted);
    str << "Expected sync mode: " << PrettyPrintSyncMode(syncMode) << "\n";
    str << "Expected cycles: " << restarts + 1 << "\n";
    SE_LOG_INFO(NULL, "sync report:\n%s\n", str.str().c_str());

    if (mustSucceed) {
        // both STATUS_OK and STATUS_HTTP_OK map to the same
        // string, so check the formatted status first, then
        // the numerical one
        CT_ASSERT_EQUAL(string("no error (remote, status 0)"), Status2String(status));
        CT_ASSERT_EQUAL(STATUS_OK, status);
    }

    for (auto &entry: report) {
        const std::string &name = entry.first;
        const SyncSourceReport &source = entry.second;

        SE_LOG_DEBUG(NULL, "Checking sync source %s...", name.c_str());
        if (mustSucceed) {
            CLIENT_TEST_EQUAL(name, STATUS_OK, source.getStatus());
        }
        check(name, source);
    }
    SE_LOG_DEBUG(NULL, "Done with checking sync report.");
}

void CheckSyncReport::check(const std::string &name, const SyncSourceReport &source) const
{
    // this code is intentionally duplicated to produce nicer CPPUNIT asserts
    CLIENT_TEST_EQUAL(name, 0, source.getItemStat(SyncSourceReport::ITEM_LOCAL,
                                                  SyncSourceReport::ITEM_ANY,
                                                  SyncSourceReport::ITEM_REJECT));
    CLIENT_TEST_EQUAL(name, 0, source.getItemStat(SyncSourceReport::ITEM_REMOTE,
                                                  SyncSourceReport::ITEM_ANY,
                                                  SyncSourceReport::ITEM_REJECT));

    const char* checkSyncModeStr = getenv("CLIENT_TEST_NOCHECK_SYNCMODE");
    bool checkSyncMode = true;
    bool checkSyncStats = getenv ("CLIENT_TEST_NOCHECK_SYNCSTATS") ? false : true;
    if (checkSyncModeStr && 
        (!strcmp(checkSyncModeStr, "1") || !strcasecmp(checkSyncModeStr, "t"))) {
        checkSyncMode = false;
    }

    if (syncMode != SYNC_NONE && checkSyncMode) {
        CLIENT_TEST_EQUAL(name, syncMode, source.getFinalSyncMode());
    }

    CLIENT_TEST_EQUAL(name, restarts + 1, source.getRestarts() + 1);

    if (clientAdded != -1 && checkSyncStats) {
        CLIENT_TEST_EQUAL(name, clientAdded,
                          source.getItemStat(SyncSourceReport::ITEM_LOCAL,
                                             SyncSourceReport::ITEM_ADDED,
                                             SyncSourceReport::ITEM_TOTAL));
    }
    if (clientUpdated != -1 && checkSyncStats) {
        CLIENT_TEST_EQUAL(name, clientUpdated,
                          source.getItemStat(SyncSourceReport::ITEM_LOCAL,
                                             SyncSourceReport::ITEM_UPDATED,
                                             SyncSourceReport::ITEM_TOTAL));
    }
    if (clientDeleted != -1 && checkSyncStats) {
        CLIENT_TEST_EQUAL(name, clientDeleted,
                          source.getItemStat(SyncSourceReport::ITEM_LOCAL,
                                             SyncSourceReport::ITEM_REMOVED,
                                             SyncSourceReport::ITEM_TOTAL));
    }

    if (serverAdded != -1 && checkSyncStats) {
        CLIENT_TEST_EQUAL(name, serverAdded,
                          source.getItemStat(SyncSourceReport::ITEM_REMOTE,
                                             SyncSourceReport::ITEM_ADDED,
                                             SyncSourceReport::ITEM_TOTAL));
    }
    if (serverUpdated != -1 && checkSyncStats) {
        CLIENT_TEST_EQUAL(name, serverUpdated,
                          source.getItemStat(SyncSourceReport::ITEM_REMOTE,
                                             SyncSourceReport::ITEM_UPDATED,
                                             SyncSourceReport::ITEM_TOTAL));
    }
    if (serverDeleted != -1 && checkSyncStats) {
        CLIENT_TEST_EQUAL(name, serverDeleted,
                          source.getItemStat(SyncSourceReport::ITEM_REMOTE,
                                             SyncSourceReport::ITEM_REMOVED,
                                             SyncSourceReport::ITEM_TOTAL));
    }
}

SE_END_CXX

#endif // ENABLE_INTEGRATION_TESTS

/** @} */
/** @endcond */
