/*
 * 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 <cstdlib>
#include <fstream>
#include <iostream>

#include <boost/test/unit_test.hpp>

#include "ServerTestHarness.hpp"
#include "TestFixture.hpp"
#include "ecflow/attribute/VerifyAttr.hpp"
#include "ecflow/core/AssertTimer.hpp"
#include "ecflow/core/DurationTimer.hpp"
#include "ecflow/node/Defs.hpp"
#include "ecflow/node/Family.hpp"
#include "ecflow/node/Suite.hpp"
#include "ecflow/node/Task.hpp"

using namespace std;
using namespace ecf;

BOOST_AUTO_TEST_SUITE(S_Test)

BOOST_AUTO_TEST_SUITE(T_CtsWaitCmd)

static void create_defs(Defs& theDefs, const std::string& suite_name) {
    // suite test_wait_cmd
    //   family family0
    //       task wait
    //   family family1
    //       task a
    //       task b
    //   endfamily
    //   family family2
    //          task aa
    //          task bb
    //    endfamily
    // endsuite
    suite_ptr suite = theDefs.add_suite(suite_name);
    family_ptr fam0 = suite->add_family("family0");
    task_ptr wait   = fam0->add_task("wait");
    wait->addVerify(VerifyAttr(NState::COMPLETE, 1)); // task should complete 1 times

    family_ptr fam1 = suite->add_family("family1");
    fam1->add_task("a");
    fam1->add_task("b");

    family_ptr fam2 = suite->add_family("family2");
    fam2->add_task("aa");
    fam2->add_task("bb");
}

static bool wait_for_state(std::vector<std::pair<std::string, NState::State>>& path_state_vec, int max_time_to_wait) {
    AssertTimer assertTimer(max_time_to_wait, false); // Bomb out after n seconds, fall back if test fail
    while (1) {
        BOOST_REQUIRE_MESSAGE(TestFixture::client().sync_local() == 0,
                              "sync_local failed should return 0\n"
                                  << TestFixture::client().errorMsg());
        defs_ptr defs      = TestFixture::client().defs();
        bool all_states_ok = true;
        for (size_t i = 0; i < path_state_vec.size(); ++i) {
            node_ptr node = defs->findAbsNode(path_state_vec[i].first);
            BOOST_REQUIRE_MESSAGE(node,
                                  "Could not find path '" << path_state_vec[i].first << "' in the defs\n"
                                                          << *defs);
            if (node->state() != path_state_vec[i].second) {
                all_states_ok = false;
                break;
            }
        }
        if (all_states_ok)
            return true;

        // make sure test does not take too long.
        if (assertTimer.duration() >= assertTimer.timeConstraint()) {
            BOOST_REQUIRE_MESSAGE(assertTimer.duration() < assertTimer.timeConstraint(),
                                  "wait_for_state: Test wait " << assertTimer.duration()
                                                               << " taking longer than time constraint of "
                                                               << assertTimer.timeConstraint() << " aborting\n"
                                                               << *defs);
            break;
        }
        sleep(1);
    }
    return false;
}

// Test the wait command. The Wait command is a child command.
// The wait command is provided an expression. This expression is evaluated
// in the server. If the evaluate returns false then the client should
// blocks until the evaluation is true.
// In this case the job associated with task 'wait' should block until the expression evaluates
// to true, which should be after the completion of all other tasks
BOOST_AUTO_TEST_CASE(test_wait_cmd) {
    DurationTimer timer;
    cout << "Test:: ...test_wait_cmd " << flush;
    TestClean clean_at_start_and_end;

    Defs theDefs;
    create_defs(theDefs, "test_wait_cmd");

    // Create a custom sms file for test_wait_cmd/family0/wait to invoke the child wait command
    // with an expression that forces it to block, until all other tasks complete
    std::string templateEcfFileForWait;
    templateEcfFileForWait += "%include <head.h>\n";
    templateEcfFileForWait += "\n";
    templateEcfFileForWait += "echo do some work\n";
    templateEcfFileForWait += "%ECF_CLIENT_EXE_PATH% --wait=\"../family1/a eq complete and ../family1/b eq complete "
                              "and ../family2/aa eq complete and ../family2/bb eq complete\"\n";
    templateEcfFileForWait += "\n";
    templateEcfFileForWait += "%include <tail.h>\n";

    // The test harness will create corresponding directory structure
    // Override the default ECF_ file, with our custom ECF_ file
    std::map<std::string, std::string> taskEcfFileMap;
    taskEcfFileMap.insert(std::make_pair(TestFixture::taskAbsNodePath(theDefs, "wait"), templateEcfFileForWait));

    ServerTestHarness serverTestHarness;
    serverTestHarness.run(theDefs, ServerTestHarness::testDataDefsLocation("test_wait_cmd.def"), taskEcfFileMap);

    cout << timer.duration() << " update-calendar-count(" << serverTestHarness.serverUpdateCalendarCount() << ")\n";
}

BOOST_AUTO_TEST_CASE(test_wait_cmd_parse_fail) {
    DurationTimer timer;
    cout << "Test:: ...test_wait_cmd_parse_fail " << flush;

    // This time we add a wait expression that
    // should fail to parse, and we should return an error
    // The error should captured by the trap in .ecf script, which will
    // then abort the task
    Defs theDefs;
    create_defs(theDefs, "test_wait_cmd_parse_fail");

    // Create a custom ecf file for test_wait_cmd/family0/wait to invoke the child wait command
    std::string templateEcfFileForWait;
    templateEcfFileForWait += "%include <head.h>\n";
    templateEcfFileForWait += "\n";
    templateEcfFileForWait += "echo do some work\n";
    templateEcfFileForWait += "%ECF_CLIENT_EXE_PATH% --wait=\"(((((((((../family1/a eq complete and ../family1/b eq "
                              "complete and ../family2/aa eq complete and ../family2/bb eq complete\"\n";
    templateEcfFileForWait += "\n";
    templateEcfFileForWait += "%include <tail.h>\n";

    std::map<std::string, std::string> taskEcfFileMap;
    taskEcfFileMap.insert(std::make_pair(TestFixture::taskAbsNodePath(theDefs, "wait"), templateEcfFileForWait));

    ServerTestHarness serverTestHarness;
    serverTestHarness.run(theDefs,
                          ServerTestHarness::testDataDefsLocation("test_wait_cmd_parse_fail.def"),
                          taskEcfFileMap,
                          1 /*timeout*/,
                          false /* don't wait for test to finish */);

    // wait for family1 and family2 to complete
    std::vector<std::pair<std::string, NState::State>> path_state_vec;
    path_state_vec.push_back(std::make_pair(std::string("/test_wait_cmd_parse_fail/family1"), NState::COMPLETE));
    path_state_vec.push_back(std::make_pair(std::string("/test_wait_cmd_parse_fail/family2"), NState::COMPLETE));
    wait_for_state(path_state_vec, 10);

    // wait for 'wait' task to abort
    path_state_vec.clear();
    path_state_vec.push_back(std::make_pair(std::string("/test_wait_cmd_parse_fail/family0/wait"), NState::ABORTED));
    wait_for_state(path_state_vec, 10);

    cout << timer.duration() << " update-calendar-count(" << serverTestHarness.serverUpdateCalendarCount() << ")\n";
}

BOOST_AUTO_TEST_CASE(test_wait_cmd_non_existant_paths) {
    DurationTimer timer;
    cout << "Test:: ...test_wait_cmd_non_existant_paths " << flush;

    // This time we add a wait expression that should fail
    // because the paths referenced in the expression don't exist
    Defs theDefs;
    create_defs(theDefs, "test_wait_cmd_non_existant_paths");

    // Create a custom ecf file for test_wait_cmd/family0/wait to invoke the child wait command
    // NOTE: ../family1/FRED does not exist
    std::string templateEcfFileForWait;
    templateEcfFileForWait += "%include <head.h>\n";
    templateEcfFileForWait += "\n";
    templateEcfFileForWait += "echo do some work\n";
    templateEcfFileForWait += "%ECF_CLIENT_EXE_PATH% --wait=\"../family1/FRED eq complete and ../family1/b eq complete "
                              "and ../family2/aa eq complete and ../family2/bb eq complete\"\n";
    templateEcfFileForWait += "\n";
    templateEcfFileForWait += "%include <tail.h>\n";

    std::map<std::string, std::string> taskEcfFileMap;
    taskEcfFileMap.insert(std::make_pair(TestFixture::taskAbsNodePath(theDefs, "wait"), templateEcfFileForWait));

    ServerTestHarness serverTestHarness;
    serverTestHarness.run(theDefs,
                          ServerTestHarness::testDataDefsLocation("test_wait_cmd_non_existant_paths.def"),
                          taskEcfFileMap,
                          1 /*timeout*/,
                          false /* don't wait for test to finish */);

    // wait for family1 and family2 to complete
    std::vector<std::pair<std::string, NState::State>> path_state_vec;
    path_state_vec.push_back(
        std::make_pair(std::string("/test_wait_cmd_non_existant_paths/family1"), NState::COMPLETE));
    path_state_vec.push_back(
        std::make_pair(std::string("/test_wait_cmd_non_existant_paths/family2"), NState::COMPLETE));
    wait_for_state(path_state_vec, 10);

    // wait for 'wait' task to abort
    path_state_vec.clear();
    path_state_vec.push_back(
        std::make_pair(std::string("/test_wait_cmd_non_existant_paths/family0/wait"), NState::ABORTED));
    wait_for_state(path_state_vec, 10);

    cout << timer.duration() << " update-calendar-count(" << serverTestHarness.serverUpdateCalendarCount() << ")\n";
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()
