/*
 * Copyright 2009- ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */

#include <fstream>
#include <iostream>
#include <string>

#include <boost/chrono.hpp>
#include <boost/test/unit_test.hpp>
#include <boost/timer/timer.hpp>

#include "PersistHelper.hpp"
#include "TemporaryFile.hpp"
#include "ecflow/core/File.hpp"
#include "ecflow/core/Log.hpp"
#include "ecflow/core/PrintStyle.hpp"
#include "ecflow/core/Str.hpp"
#include "ecflow/node/Defs.hpp"
#include "ecflow/node/Family.hpp"
#include "ecflow/node/Jobs.hpp"
#include "ecflow/node/JobsParam.hpp"
#include "ecflow/node/NodeContainer.hpp"
#include "ecflow/node/Suite.hpp"
#include "ecflow/node/System.hpp"
#include "ecflow/node/Task.hpp"
#include "ecflow/node/parser/DefsStructureParser.hpp"

using namespace std;
using namespace ecf;
using namespace boost::posix_time;

BOOST_AUTO_TEST_SUITE(U_Parser)

BOOST_AUTO_TEST_SUITE(T_SingleDefsFile)

// This test is used to find a task given a path of the form:
//     suite/family/task
//     suite/family/family/task
void test_find_task_using_path(NodeContainer* f, const Defs& defs) {
    BOOST_CHECK_MESSAGE(f == defs.findAbsNode(f->absNodePath()).get(),
                        "Could not find path " << f->absNodePath() << "\n");

    for (node_ptr t : f->nodeVec()) {
        BOOST_CHECK_MESSAGE(t.get() == defs.findAbsNode(t->absNodePath()).get(),
                            "Could not find path " << t->absNodePath() << "\n");
        Family* family = t->isFamily();
        if (family) {
            test_find_task_using_path(family, defs);
        }
    }
}

// Create derived class, so that we can time the parse only
// i.e ignore expression build/checking and limit checking
class TestDefsStructureParser : public DefsStructureParser {
public:
    TestDefsStructureParser(Defs* defsfile, const std::string& file_name) : DefsStructureParser(defsfile, file_name) {}
    bool do_parse_file(std::string& errorMsg) { return DefsStructureParser::do_parse_file(errorMsg); }
};

double get_seconds(const boost::timer::nanosecond_type& elapsed) {
    return static_cast<double>(boost::chrono::nanoseconds(elapsed).count()) * boost::chrono::nanoseconds::period::num /
           boost::chrono::nanoseconds::period::den;
}

BOOST_AUTO_TEST_CASE(test_single_defs) {
    boost::timer::auto_cpu_timer auto_cpu_timer;

    std::string path      = File::test_data("libs/node/test/parser/data/single_defs/mega.def", "parser");
    size_t mega_file_size = fs::file_size(path);
    cout << "AParser:: ...test_single_defs " << path << " file_size(" << mega_file_size << ")\n";

    // Time parse/resolve dependencies: This will need to be #defined depending on the platform
    // Change for file_iterator to plain string halved the time taken to load operation suite
    boost::timer::cpu_timer timer;

#ifdef DEBUG
    #if defined(HPUX) || defined(_AIX)
    double expectedTimeForParse                   = 15.0;
    double expectedTimeForParseOnly               = 10.0;
    double expectedTimeForResolveDependencies     = 3.5; // this is time for 10 job submissions
    double checkExprAndLimits                     = 1.0;
    double expectedTimeForFindAllPaths            = 7.2;
    double expectedTimeForDefsPersistOnly         = 6;
    double expectedTimeForDefsPersistAndReload    = 24;
    double expectedTimeForCheckPtPersistAndReload = 27;
    #else
    double expectedTimeForParse                   = 4.5;
    double expectedTimeForParseOnly               = 2.0;
    double expectedTimeForResolveDependencies     = 0.5; // this is time for 10 job submissions
    double checkExprAndLimits                     = 0.5;
    double expectedTimeForFindAllPaths            = 1.2;
    double expectedTimeForDefsPersistOnly         = 2;
    double expectedTimeForDefsPersistAndReload    = 4.5;
    double expectedTimeForCheckPtPersistAndReload = 8.0;
    #endif
#else
    #if defined(HPUX) || defined(_AIX)
    double expectedTimeForParse                   = 7.8;
    double expectedTimeForParseOnly               = 5.0;
    double expectedTimeForResolveDependencies     = 3.5; // this is time for 10 job submissions
    double checkExprAndLimits                     = 1.0;
    double expectedTimeForFindAllPaths            = 4.5;
    double expectedTimeForDefsPersistOnly         = 3.5;
    double expectedTimeForDefsPersistAndReload    = 9.5;
    double expectedTimeForCheckPtPersistAndReload = 11.5;
    #else
    double expectedTimeForParse                   = 1.2;
    double expectedTimeForParseOnly               = 0.6;
    double expectedTimeForResolveDependencies     = 0.2; // this is time for 10 job submissions
    double checkExprAndLimits                     = 0.1;
    double expectedTimeForFindAllPaths            = 0.58;
    double expectedTimeForDefsPersistOnly         = 0.4;
    double expectedTimeForDefsPersistAndReload    = 1.4;
    double expectedTimeForCheckPtPersistAndReload = 1.6;
    #endif
#endif
    // ****************************************************************************************
    // Please note: that for Parsing: that the predominate time is taken in creating the AST/ and checking

    /// If this is moved below, we get some caching affect, with the persist and reload timing
    Defs defs;
    {
        timer.start();
        std::string errorMsg, warningMsg;
        BOOST_REQUIRE_MESSAGE(defs.restore(path, errorMsg, warningMsg), errorMsg);
        BOOST_CHECK_MESSAGE(get_seconds(timer.elapsed().user) < expectedTimeForParse,
                            "Performance regression, expected < " << expectedTimeForParse
                                                                  << " seconds for parse/node tree creation but found "
                                                                  << timer.format(3, Str::cpu_timer_format()));
        cout << " Parsing Node tree and AST creation time                = " << timer.format(3, Str::cpu_timer_format())
             << " < limit(" << expectedTimeForParse << ")" << endl;
    }
    {
        Defs local_defs;
        timer.start();
        TestDefsStructureParser checkPtParser(&local_defs, path);
        std::string errorMsg;
        BOOST_REQUIRE_MESSAGE(checkPtParser.do_parse_file(errorMsg), errorMsg);
        BOOST_CHECK_MESSAGE(get_seconds(timer.elapsed().user) < expectedTimeForParse,
                            "Performance regression, expected < " << expectedTimeForParseOnly
                                                                  << " seconds for parse/node tree creation but found "
                                                                  << timer.format(3, Str::cpu_timer_format()));
        cout << " Parsing Node tree *only* time                          = " << timer.format(3, Str::cpu_timer_format())
             << " < limit(" << expectedTimeForParseOnly << ")" << endl;
    }
    {
        timer.start();
        for (suite_ptr s : defs.suiteVec()) {
            test_find_task_using_path(s.get(), defs);
        }
        BOOST_CHECK_MESSAGE(get_seconds(timer.elapsed().user) < expectedTimeForFindAllPaths,
                            "Performance regression, expected < " << expectedTimeForFindAllPaths
                                                                  << " seconds to find all paths, but found "
                                                                  << timer.format(3, Str::cpu_timer_format()));
        cout << " Test all paths can be found. time taken                = " << timer.format(3, Str::cpu_timer_format())
             << " < limit(" << expectedTimeForFindAllPaths << ")" << endl;
    }

    {
        // Test time for persisting to defs file only
        TemporaryFile temporary("tmp_%%%%-%%%%-%%%%-%%%%.def");

        timer.start();
        PrintStyle style(PrintStyle::DEFS);
        std::ofstream ofs(temporary.path());
        ofs << defs;
        BOOST_CHECK_MESSAGE(get_seconds(timer.elapsed().user) < expectedTimeForDefsPersistOnly,
                            "Performance regression, expected < " << expectedTimeForDefsPersistOnly
                                                                  << " to persist defs file, but found "
                                                                  << timer.format(3, Str::cpu_timer_format()));
        cout << " Persist only, time taken                               = " << timer.format(3, Str::cpu_timer_format())
             << " < limit(" << expectedTimeForDefsPersistOnly << ")" << endl;
    }

    {
        // may need to comment out output for large differences. Will double the time.
        timer.start();
        PersistHelper helper;
        BOOST_CHECK_MESSAGE(helper.test_persist_and_reload(defs, PrintStyle::DEFS), helper.errorMsg());
        BOOST_CHECK_MESSAGE(get_seconds(timer.elapsed().user) < expectedTimeForDefsPersistAndReload,
                            "Performance regression, expected < " << expectedTimeForDefsPersistAndReload
                                                                  << " seconds to persist and reload, but found "
                                                                  << timer.format(3, Str::cpu_timer_format()));
        cout << " Persist and reload(DEFS) and compare, time taken       = " << timer.format(3, Str::cpu_timer_format())
             << " < limit(" << expectedTimeForDefsPersistAndReload << ")"
             << " file_size(" << helper.file_size() << ")" << endl;
    }
    {
        timer.start();
        PersistHelper helper;
        BOOST_CHECK_MESSAGE(helper.test_persist_and_reload(defs, PrintStyle::STATE), helper.errorMsg());
        cout << " Persist and reload(STATE) and compare, time taken      = " << timer.format(3, Str::cpu_timer_format())
             << " file_size(" << helper.file_size() << ")" << endl;
    }
    {
        timer.start();
        PersistHelper helper;
        BOOST_CHECK_MESSAGE(helper.test_persist_and_reload(defs, PrintStyle::MIGRATE), helper.errorMsg());
        BOOST_CHECK_MESSAGE(get_seconds(timer.elapsed().user) < expectedTimeForDefsPersistAndReload,
                            "Performance regression, expected < "
                                << expectedTimeForDefsPersistAndReload
                                << " seconds to persist and reload *state*, but found "
                                << timer.format(3, Str::cpu_timer_format()));
        cout << " Persist and reload(MIGRATE) and compare, time taken    = " << timer.format(3, Str::cpu_timer_format())
             << " < limit(" << expectedTimeForDefsPersistAndReload << ")"
             << " file_size(" << helper.file_size() << ")" << endl;

        // each platform will have a slightly different size, since the server environment variables
        // will be different, i.e host, pid, i.e check point etc, encompasses the host name, which will be different
        BOOST_CHECK_MESSAGE(helper.file_size() <= 6679000,
                            "File size regression expected <= 6679000 but found " << helper.file_size());
    }
    {
        timer.start();
        PersistHelper helper;
        BOOST_CHECK_MESSAGE(helper.test_persist_and_reload(defs, PrintStyle::NET), helper.errorMsg());
        BOOST_CHECK_MESSAGE(get_seconds(timer.elapsed().user) < expectedTimeForDefsPersistAndReload,
                            "Performance regression, expected < "
                                << expectedTimeForDefsPersistAndReload
                                << " seconds to persist and reload *state*, but found "
                                << timer.format(3, Str::cpu_timer_format()));
        cout << " Persist and reload(NET) and compare, time taken        = " << timer.format(3, Str::cpu_timer_format())
             << " < limit(" << expectedTimeForDefsPersistAndReload << ")"
             << " file_size(" << helper.file_size() << ")" << endl;

        // each platform will have a slightly different size, since the server environment variables
        // will be different, i.e host, pid, i.e check point etc, encompasses the host name, which will be different
        BOOST_CHECK_MESSAGE(helper.file_size() <= 6679000,
                            "File size regression expected <= 6679000 but found " << helper.file_size());
    }

    {
        timer.start();
        PersistHelper helper;
        BOOST_CHECK_MESSAGE(helper.test_cereal_checkpt_and_reload(defs, true), helper.errorMsg());
        BOOST_CHECK_MESSAGE(get_seconds(timer.elapsed().user) < expectedTimeForCheckPtPersistAndReload,
                            "Performance regression, expected < " << expectedTimeForCheckPtPersistAndReload
                                                                  << " seconds to persist and reload, but found "
                                                                  << timer.format(3, Str::cpu_timer_format()));
        cout << " Checkpt(CEREAL) and reload and compare, time taken     = ";
        cout << timer.format(3, Str::cpu_timer_format()) << " < limit(" << expectedTimeForCheckPtPersistAndReload << ")"
             << " file_size(" << helper.file_size() << ")" << endl;
    }

    {
        // Time how long it takes for job submission. Must call begin on all suites first.
        timer.start();
        defs.beginAll();
        int count = 10;
        JobsParam jobsParam; // default is *not* to create jobs, and *not* to spawn jobs, hence only used in testing
        Jobs jobs(&defs);
        for (int i = 0; i < count; i++) {
            jobs.generate(jobsParam);
        }
        cout << " " << count
             << " jobSubmissions                                      = " << timer.format(3, Str::cpu_timer_format())
             << "s jobs:" << jobsParam.submitted().size() << " < limit(" << expectedTimeForResolveDependencies << ")"
             << endl;
        BOOST_CHECK_MESSAGE(get_seconds(timer.elapsed().user) < expectedTimeForResolveDependencies,
                            "jobSubmission Performance regression, expected < "
                                << expectedTimeForResolveDependencies
                                << " seconds for resolving dependencies but found "
                                << timer.format(3, Str::cpu_timer_format()));
    }

    {
        // Time how long it takes for post process
        timer.start();
        string errorMsg, warningMsg;
        BOOST_CHECK(defs.check(errorMsg, warningMsg));
        cout << " Defs::check (inlimit resolution)                       = " << timer.format(3, Str::cpu_timer_format())
             << " < limit(" << checkExprAndLimits << ")" << endl;
        BOOST_CHECK_MESSAGE(get_seconds(timer.elapsed().user) < checkExprAndLimits,
                            "Defs::check Performance regression, expected < "
                                << checkExprAndLimits << " seconds for resolving dependencies but found "
                                << timer.format(3, Str::cpu_timer_format()));
    }

    {
        // Time how long it takes to delete all nodes/ references. Delete all tasks and then suites/families.
        timer.start();
        std::vector<Task*> tasks;
        defs.getAllTasks(tasks);
        BOOST_CHECK_MESSAGE(tasks.size() > 0, "Expected > 0 tasks but found " << tasks.size());
        for (Task* t : tasks) {
            BOOST_REQUIRE_MESSAGE(defs.deleteChild(t), " Failed to delete task");
        }
        tasks.clear();
        defs.getAllTasks(tasks);
        BOOST_REQUIRE_MESSAGE(tasks.empty(), "Expected all tasks to be deleted but found " << tasks.size());

        std::vector<suite_ptr> vec = defs.suiteVec(); // make a copy, to avoid invalidating iterators
        BOOST_CHECK_MESSAGE(vec.size() > 0, "Expected > 0 Suites but found " << vec.size());
        for (suite_ptr s : vec) {
            std::vector<node_ptr> familyVec = s->nodeVec(); // make a copy, to avoid invalidating iterators
            for (node_ptr f : familyVec) {
                BOOST_REQUIRE_MESSAGE(defs.deleteChild(f.get()), " Failed to delete family");
            }
            BOOST_REQUIRE_MESSAGE(s->nodeVec().empty(),
                                  "Expected all Families to be deleted but found " << s->nodeVec().size());
            BOOST_REQUIRE_MESSAGE(defs.deleteChild(s.get()), " Failed to delete suite");
        }
        BOOST_REQUIRE_MESSAGE(defs.suiteVec().empty(),
                              "Expected all Suites to be deleted but found " << defs.suiteVec().size());

        cout << " time for deleting all nodes                            = " << timer.format(3, Str::cpu_timer_format())
             << endl;
    }

    // Explicitly destroy, To keep valgrind happy
    Log::destroy();
    System::destroy();
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()
