/*
 * Copyright (C) 2005-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
 */

#include "config.h"

#include <ClientTest.h>

#include <exception>
#include <fstream>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#include "CmdlineSyncClient.h"
#include <syncevo/SyncSource.h>
#include <syncevo/util.h>
#include <syncevo/VolatileConfigNode.h>

#include <boost/bind.hpp>

#include <syncevo/declarations.h>

SE_BEGIN_CXX

/*
 * always provide this test class, even if not used:
 * that way the test scripts can unconditionally
 * invoke "client-test SyncEvolution"
 */
CPPUNIT_REGISTRY_ADD_TO_DEFAULT("SyncEvolution");

class EvolutionLocalTests : public LocalTests {
public:
    EvolutionLocalTests(const std::string &name, ClientTest &cl, int sourceParam, ClientTest::Config &co) :
        LocalTests(name, cl, sourceParam, co)
        {}

    virtual void addTests() {
        LocalTests::addTests();
    }
};

/**
 * This is a workaround for libecal/libebook in Evolution >= 2.30.
 * The storage daemons shut down after only 10 seconds of no client
 * being attached.  Due to limitations in libecal/libebook this is not
 * detected when only using the synchronous API ("destroyed" signal
 * not delivered, see e-cal.c), which then leads to D-Bus errors.
 *
 * The workaround consists of keeping one open SyncEvolution backend
 * around for each of eds_event and eds_contact/30, if they ever were used
 * during testing.
 */
static map<string, boost::shared_ptr<TestingSyncSource> > lockEvolution;
static void CleanupSources()
{
    lockEvolution.clear();
}

/**
 * This code uses the ClientTest and and information provided by
 * the backends in their RegisterSyncSourceTest instances to test
 * real synchronization with a server.
 *
 * Configuration is done by environment variables which indicate which
 * part below the root node "client-test" of the the configuration tree to use;
 * beyond that everything needed for synchronization is read from the
 * configuration tree.
 *
 * - CLIENT_TEST_SERVER = maps to name of root node in configuration tree
 * - CLIENT_TEST_EVOLUTION_PREFIX = a common "evolutionsource" prefix for *all*
 *                                  sources; the source name followed by "_[12]"
 *                                  is appended to get unique names
 * - CLIENT_TEST_EVOLUTION_USER = sets the "evolutionuser" property of all sources
 * - CLIENT_TEST_EVOLUTION_PASSWORD = sets the "evolutionpassword" property of all sources
 * - CLIENT_TEST_SOURCES = comma separated list of active sources,
 *                         names as selected in their RegisterSyncSourceTest
 *                         instances
 * - CLIENT_TEST_DELAY = number of seconds to sleep between syncs, required
 *                       by some servers
 * - CLIENT_TEST_LOG = logfile name of a server, can be empty:
 *                     if given, then the content of that file will be
 *                     copied and stored together with the client log
 *                     (only works on Unix)
 * - CLIENT_TEST_NUM_ITEMS = numbers of contacts/events/... to use during
 *                           local and sync tests which create artificial
 *                           items
 *
 * The CLIENT_TEST_SERVER also has another meaning: it is used as hint
 * by the synccompare.pl script and causes it to automatically ignore
 * known, acceptable data modifications caused by sending an item to
 * a server and back again. Currently the script recognizes "funambol",
 * "scheduleworld", "synthesis" and "egroupware" as special server
 * names.
 */
class TestEvolution : public ClientTest {
public:
    /**
     * can be instantiated as client A with id == "1" and client B with id == "2"
     */
    TestEvolution(const string &id) :
        ClientTest(atoi(getEnv("CLIENT_TEST_DELAY", "0")),
                   getEnv("CLIENT_TEST_LOG", "")),
        m_initialized(false),
        m_clientID(id),
        m_configs(SyncSource::getTestRegistry())
    {
    }

    virtual std::string getClientID() const { return m_clientID; }

    /**
     * code depends on other global constructors to run first, execute it after constructor but before
     * any other methods
     */
    void init()
    {
        if (m_initialized) {
	    return;
        } else {
            m_initialized = true;
        }

        // allow each backend test to create more backend tests
        size_t count = 0;
        while (count != m_configs.size()) {
            count = m_configs.size();
            BOOST_FOREACH (const RegisterSyncSourceTest *test,
                           TestRegistry(m_configs)) {
                test->init();
            }
        }

        const char *server = getenv("CLIENT_TEST_SERVER");

        if (m_clientID == "1") {
            m_clientB.reset(new TestEvolution("2"));
        }

        /* check server */
        if (!server) {
            server = "funambol";
            setenv("CLIENT_TEST_SERVER", "funambol", 1);
        }

        /* override Evolution database names? */
        const char *evoprefix = getenv("CLIENT_TEST_EVOLUTION_PREFIX");
        m_evoPrefix = evoprefix ? evoprefix :  "SyncEvolution_Test_";
        const char *evouser = getenv("CLIENT_TEST_EVOLUTION_USER");
        if (evouser) {
            m_evoUser = evouser;
        }
        const char *evopasswd = getenv("CLIENT_TEST_EVOLUTION_PASSWORD");
        if (evopasswd) {
            m_evoPassword = evopasswd;
        }

        /* check sources */
        const char *sourcelist = getenv("CLIENT_TEST_SOURCES");
        set<string> sources;
        if (sourcelist) {
            boost::split(sources, sourcelist, boost::is_any_of(","));
        } else {
            BOOST_FOREACH(const RegisterSyncSourceTest *test, m_configs) {
                if (!test->m_configName.empty()) {
                    sources.insert(test->m_configName);
                }
            }
        }

        BOOST_FOREACH(const RegisterSyncSourceTest *test, m_configs) {
            if (sources.find(test->m_configName) != sources.end() &&
                !test->m_configName.empty()) {
                m_syncSource2Config.push_back(test->m_configName);
            }
        }

        /* Local Test SyncSource : remove all virtual datastores, inserting the
         * sub datastores*/
        ClientTest::Config conf;
        BOOST_FOREACH (string source, sources) {
            getSourceConfig (source, conf);
            if (!conf.m_subConfigs.empty()) {
                vector<string> subs;
                boost::split (subs, conf.m_subConfigs, boost::is_any_of(","));
                BOOST_FOREACH (string sub, subs) {
                    pushLocalSource2Config(sub);
                }
            } else {
                pushLocalSource2Config(source);
            }
        }
        // get configuration and set obligatory fields
        Logger::instance().setLevel(Logger::DEBUG);
        std::string root = std::string("evolution/") + server + "_" + m_clientID;
        boost::shared_ptr<SyncConfig> config(new SyncConfig(string(server) + "_" + m_clientID));
        boost::shared_ptr<SyncConfig> from = boost::shared_ptr<SyncConfig> ();

        if (!config->exists()) {
            // no configuration yet, create in different contexts because
            // device ID is different
            config.reset(new SyncConfig(string(server) + "_" + m_clientID + "@client-test-" + m_clientID));
            config->setDefaults();
            from = SyncConfig::createPeerTemplate(server);
            if(from) {
                set<string> filter;
                config->copy(*from, &filter);
            }
            config->setDevID(m_clientID == "1" ? "sc-api-nat" : "sc-pim-ppc");
        }
        BOOST_FOREACH(const RegisterSyncSourceTest *test, m_configs) {
            if (test->m_configName.empty()) {
                continue;
            }

            ClientTest::Config testconfig;
            getSourceConfig(test, testconfig);
            CPPUNIT_ASSERT(!testconfig.m_type.empty());

            boost::shared_ptr<SyncSourceConfig> sc = config->getSyncSourceConfig(testconfig.m_sourceName);
            if (!sc || !sc->exists()) {
                // no configuration yet
                config->setSourceDefaults(testconfig.m_sourceName);
                sc = config->getSyncSourceConfig(testconfig.m_sourceName);
                CPPUNIT_ASSERT(sc);
                sc->setURI(testconfig.m_uri);
                if(from && !testconfig.m_sourceNameServerTemplate.empty()) {
                    boost::shared_ptr<SyncSourceConfig> scServerTemplate = from->getSyncSourceConfig(testconfig.m_sourceNameServerTemplate);
                    sc->setURI(scServerTemplate->getURI());
                }
            }

            // Set these properties if not set yet: that means the env
            // variables are used when creating the config initially,
            // but then no longer can be used to change the config.
            // This prevents accidentally running a test with default
            // values, for example for the database.
            if (!sc->getDatabaseID().wasSet()) {
                string database = getDatabaseName(test->m_configName);
                sc->setDatabaseID(database);
            }
            if (!sc->getUser().wasSet() && !m_evoUser.empty()) {
                sc->setUsername(m_evoUser);
            }
            if (!sc->getPassword().wasSet() && !m_evoPassword.empty()) {
                sc->setPassword(m_evoPassword);
            }
            // Always set this one, to ensure the config matches the test.
            sc->setBackend(SourceType(testconfig.m_type).m_backend);
        }
        config->flush();
    }

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

    virtual int getNumLocalSources() {
        init();
        return m_localSource2Config.size();
    }

    virtual int getNumSyncSources() {
        init();
        return m_syncSource2Config.size();
    }

    virtual void getLocalSourceConfig(int source, Config &config) {
        init();
        getSourceConfig(m_configs[m_localSource2Config[source]], config);
    }

    virtual void getSyncSourceConfig(int source, Config &config) {
        init();
        getSourceConfig(m_configs[m_syncSource2Config[source]], config);
    }

    virtual int getLocalSourcePosition(const string &configName) {
        for (size_t i=0; i< m_localSource2Config.size(); i++) {
            if(m_localSource2Config[i] == configName) {
                return i;
                break;
            }
        }
        return -1;
    }

    virtual void getSourceConfig (const string &configName, Config &config) {
        init();
        return getSourceConfig (m_configs[configName], config);
    }

    static void getSourceConfig(const RegisterSyncSourceTest *test, Config &config) {
        ClientTest::getTestData(test->m_testCaseName.c_str(), config);
        config.m_createSourceA = createSource;
        config.m_createSourceB = createSource;
        config.m_sourceName = test->m_configName.c_str();
        config.m_linkedSources = test->m_linkedSources;

        test->updateConfig(config);
    }

    virtual ClientTest *getClientB() {
        init();
        return m_clientB.get();
    }

    virtual bool isB64Enabled() {
        return false;
    }

    virtual SyncMLStatus doSync(const int *sources,
                                const std::string &logbase,
                                const SyncOptions &options)
    {
        init();

        // Let "Client_Sync_Current" symlink point to a new, empty
        // directory logbase + ".server". Can be used by SyncEvolution
        // server as per-test logdir.
        std::string current = logbase + ".server";
        rm_r(current);
        mkdir_p(current);
        rm_r("Client_Sync_Current");
        symlink(current.c_str(), "Client_Sync_Current");

        string server = getenv("CLIENT_TEST_SERVER") ? getenv("CLIENT_TEST_SERVER") : "funambol";
        server += "_";
        server += m_clientID;
        

        class ClientTest : public CmdlineSyncClient {
        public:
            ClientTest(const string &server,
                       const string &logbase,
                       const SyncOptions &options) :
                CmdlineSyncClient(server, false),
                m_logbase(logbase),
                m_options(options),
                m_started(false)
                {}

        protected:
            virtual void prepare() {
                setLogDir(m_logbase, true);
                setMaxLogDirs(0, true);
                setMaxObjSize(m_options.m_maxObjSize, true);
                setMaxMsgSize(m_options.m_maxMsgSize, true);
                setWBXML(m_options.m_isWBXML, true);
                setRetryDuration(m_options.m_retryDuration, true);
                setRetryInterval(m_options.m_retryInterval, true);
                if (m_options.m_syncMode == SYNC_TWO_WAY &&
                    m_options.m_checkReport.syncMode == SYNC_NONE) {
                    // For this test, any kind of final sync mode is
                    // acceptable. Disable slow sync prevention
                    // temporarily. The check for the requested sync
                    // mode is perhaps too conservative, but in
                    // practice the only test where slow sync
                    // prevention caused a test failure was
                    // Client::Sync::eds_contact::testTwoWaySync after
                    // some other failed test, so let's be conservative...
                    setPreventSlowSync(false);
                }
                SyncContext::prepare();
                if (m_options.m_prepareCallback &&
                    m_options.m_prepareCallback(*this, m_options)) {
                    m_options.m_isAborted = SuspendFlags::getSuspendFlags().abort();
                }
            }

            virtual void displaySyncProgress(sysync::TProgressEventEnum type,
                                             int32_t extra1, int32_t extra2, int32_t extra3)
            {
                if (!m_started) {
                    m_started = true;
                    if (m_options.m_startCallback(*this, m_options)) {
                        m_options.m_isAborted = SuspendFlags::getSuspendFlags().abort();
                    }
                }
            }

            virtual boost::shared_ptr<TransportAgent> createTransportAgent()
            {
                boost::shared_ptr<TransportAgent>wrapper = m_options.m_transport;
                boost::shared_ptr<TransportAgent>agent =SyncContext::createTransportAgent();
                if (!wrapper.get())
                    return agent;
                dynamic_cast<TransportWrapper*>(wrapper.get())->setAgent(agent);
                dynamic_cast<TransportWrapper*>(wrapper.get())->setSyncOptions(&m_options);
                return wrapper;
            }

        private:
            const string m_logbase;
            SyncOptions m_options;
            bool m_started;
        } client(server, logbase, options);

        // configure active sources with the desired sync mode,
        // disable the rest
        FilterConfigNode::ConfigFilter filter;
        filter["sync"] = InitStateString("none", true);
        client.setConfigFilter(false, "", filter);
        filter["sync"] =
            InitStateString(PrettyPrintSyncMode(options.m_syncMode), true);
        for(int i = 0; sources[i] >= 0; i++) {
            std::string &name = m_syncSource2Config[sources[i]];
            client.setConfigFilter(false, name, filter);
            checkEvolutionSource(name);
        }

        SyncReport report;
        SyncMLStatus status = client.sync(&report);
        options.m_checkReport.check(status, report);
        return status;
    }
  
private:
    bool m_initialized;
    string m_clientID;
    std::unique_ptr<TestEvolution> m_clientB;
    const TestRegistry &m_configs;

    /** prefix, username, password to be used for local databases */
    string m_evoPrefix, m_evoUser, m_evoPassword;

    /**
     * The ClientTest framework identifies active configs with an integer.
     * This is the mapping to the corresponding config name, created when
     * constructing this instance.
     */
    vector<string> m_localSource2Config;
    vector<string> m_syncSource2Config;

    /** returns the name of the Evolution database */
    string getDatabaseName(const string &configName) {
        if (configName == "calendar+todo") {
            return "eds_event,eds_task";
        } else if (configName == "file_calendar+todo") {
            return "file_event,file_task";
        }
        return m_evoPrefix + configName + "_" + m_clientID;
    }

    /** called by test frame work */
    static TestingSyncSource *createSource(ClientTest &client, const std::string &clientID, int source, bool isSourceA) {
        TestEvolution &evClient((TestEvolution &)client);
        string name = evClient.m_localSource2Config[source];

        // implement Evolution shutdown workaround (see lockEvolution above)
        evClient.checkEvolutionSource(name);

        return evClient.createNamedSource(name, isSourceA);
    }

    /** called internally in this class */
    TestingSyncSource *createNamedSource(const string &name, bool isSourceA) {
        string database = getDatabaseName(name);
        std::string config = "target-config@client-test";
        const char *server = getenv("CLIENT_TEST_SERVER");
        if (server) {
            config += "-";
            config += server;
        }
        std::string tracking =
            string("_") + m_clientID +
            "_" + (isSourceA ? "A" : "B");
        SE_LOG_DEBUG(NULL, "instantiating testing source %s in config %s, with tracking name %s",
                     name.c_str(),
                     config.c_str(),
                     tracking.c_str());
        boost::shared_ptr<SyncConfig> context(new SyncConfig(config));
        SyncSourceNodes nodes = context->getSyncSourceNodes(name, tracking);

        // The user of client-test must have configured the source
        // @<CLIENT_TEST_SERVER>_<m_clientID>/<name> when doing
        // Client::Sync testing.  Our testing source must use the same
        // properties, but different change tracking.
        std::string peerName = server ? (std::string(server) + "_" + m_clientID) : "@default";
        boost::shared_ptr<SyncConfig> peer(new SyncConfig(peerName));
        SyncSourceNodes peerNodes = peer->getSyncSourceNodes(name);
        SE_LOG_DEBUG(NULL, "overriding testing source %s properties with the ones from config %s = %s",
                     name.c_str(),
                     peerName.c_str(),
                     peer->getRootPath().c_str());
        BOOST_FOREACH(const ConfigProperty *prop, SyncSourceConfig::getRegistry()) {
            if (prop->isHidden()) {
                continue;
            }
            boost::shared_ptr<FilterConfigNode> node = peerNodes.getNode(*prop);
            InitStateString value = prop->getProperty(*node);
            SE_LOG_DEBUG(NULL, "   %s = %s (%s)",
                         prop->getMainName().c_str(),
                         value.c_str(),
                         value.wasSet() ? "set" : "default");
            node = nodes.getNode(*prop);
            node->setProperty(prop->getMainName(), value);
        }
        context->flush();

        // Same as in init() above: set values if still empty, but don't
        // overwrite anything.
        boost::shared_ptr<FilterConfigNode> props = nodes.getProperties();
        std::string value;
        if (!props->getProperty("database", value)) {
            props->setProperty("database", database);
        }
        if (!props->getProperty("databaseUser", value) &&
            !m_evoUser.empty()) {
            props->setProperty("databaseUser", m_evoUser);
        }
        if (!props->getProperty("databasePassword", value) &&
            !m_evoPassword.empty()) {
            props->setProperty("databasePassword", m_evoPassword);
        }

        SyncSourceParams params(name,
                                nodes,
                                context);
        const RegisterSyncSourceTest *test = m_configs[name];
        ClientTestConfig testConfig;
        getSourceConfig(test, testConfig);

        PersistentSyncSourceConfig sourceConfig(params.m_name, params.m_nodes);
        sourceConfig.setSourceType(SourceType(testConfig.m_type));

        // downcasting here: anyone who registers his sources for testing
        // must ensure that they are indeed TestingSyncSource instances
        SyncSource *ss = SyncSource::createSource(params);
        return static_cast<TestingSyncSource *>(ss);
    }

    // push source into localsource2config if it doesn't exist in the vector
    void pushLocalSource2Config(const string &source) {
        bool finded = false;
        BOOST_FOREACH(string element, m_localSource2Config) {
            if (boost::iequals(element, source)) {
                finded = true;
                break;
            }
        }
        if (!finded) {
            m_localSource2Config.push_back (source);
        }
    }

    void checkEvolutionSource(std::string &name)
    {
        string basename;

        // hard-coded names as used by src/backends/evolution;
        // if some other backend reuses them, it gets the
        // same treatment, which shouldn't cause any harm
        if (name == "eds_contact") {
            basename = "ebook";
        } else if (name == "eds_event" ||
                   name == "text") {
            basename = "ecal";
        }

        if (!basename.empty() &&
            lockEvolution.find(basename) == lockEvolution.end()) {
            lockEvolution[basename].reset(createNamedSource(name, true));
            lockEvolution[basename]->open();
            ClientTest::registerCleanup(CleanupSources);
        }
    }
};

static class RegisterTestEvolution {
public:
    RegisterTestEvolution() :
        testClient("1") {
        testClient.registerTests();
    }

private:
    TestEvolution testClient;
    
} testEvolution;

SE_END_CXX
