/*
 * 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 <iostream>

#include <boost/test/unit_test.hpp>

#include "MockServer.hpp"
#include "ecflow/base/cts/user/FreeDepCmd.hpp"
#include "ecflow/base/stc/ServerToClientCmd.hpp"
#include "ecflow/node/Defs.hpp"
#include "ecflow/node/Expression.hpp"
#include "ecflow/node/Family.hpp"
#include "ecflow/node/Suite.hpp"
#include "ecflow/node/Task.hpp"

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

BOOST_AUTO_TEST_SUITE(U_Base)

BOOST_AUTO_TEST_SUITE(T_FreeDepCmd)

BOOST_AUTO_TEST_CASE(test_free_dep_cmd) {
    cout << "Base:: ...test_free_dep_cmd\n";
    TestLog test_log("test_free_dep_cmd.log"); // will create log file, and destroy log and remove file at end of scope

    // Create a test and add the date and time dependencies
    ptime theLocalTime(date(2011, Nov, 4), time_duration(9, 30, 0));
    ptime time_plus_hour   = theLocalTime + hours(1);
    ptime time_plus_2_hour = theLocalTime + hours(2);
    date todaysDate        = theLocalTime.date();
    date tomorrows_date    = todaysDate + date_duration(1);
    date tomorrows_date_1  = todaysDate + date_duration(2);

    // Get tomorrow as a day so that isFree fails.
    tm day_1 = to_tm(tomorrows_date);
    tm day_2 = to_tm(tomorrows_date_1);

    suite_ptr suite;
    task_ptr task;
    Defs theDefs;
    {
        suite = theDefs.add_suite("test_time");
        ClockAttr clockAttr(theLocalTime, false /*false means use real clock*/);
        suite->addClock(clockAttr);

        family_ptr fam = suite->add_family("family");
        task           = fam->add_task("t");
        task->addTime(ecf::TimeAttr(TimeSlot(time_plus_hour.time_of_day())));
        task->addTime(ecf::TimeAttr(TimeSlot(time_plus_2_hour.time_of_day())));
        task->addTime(ecf::TimeAttr(
            ecf::TimeSlot(1, 1), ecf::TimeSlot(1, 3), ecf::TimeSlot(0, 1), true /*relative to suite start*/
            ));
        task->addToday(ecf::TodayAttr(TimeSlot(time_plus_hour.time_of_day())));
        task->addToday(ecf::TodayAttr(TimeSlot(time_plus_2_hour.time_of_day())));
        task->addDate(DateAttr(tomorrows_date));
        task->addDate(DateAttr(tomorrows_date_1));
        task->addDay(DayAttr(static_cast<DayAttr::Day_t>(day_1.tm_wday)));
        task->addDay(DayAttr(static_cast<DayAttr::Day_t>(day_2.tm_wday)));
        task->add_trigger("t2 == complete");

        CronAttr cronAttr;
        cronAttr.addTimeSeries(ecf::TimeSlot(time_plus_hour.time_of_day()),
                               ecf::TimeSlot(time_plus_2_hour.time_of_day()),
                               ecf::TimeSlot(0, 1));
        task->addCron(cronAttr);

        fam->add_task("t2");
        //  	cout << theDefs << "\n";
    }
    std::vector<task_ptr> vec;
    theDefs.get_all_tasks(vec);
    BOOST_REQUIRE_MESSAGE(vec.size() == 2, "Error in number of tasks");

    // This will initialise the calendar from the Clock attribute
    theDefs.beginAll();

    // First all trigger should fail to evaluate AND all dependencies should NOT be free
    BOOST_REQUIRE_MESSAGE(task->triggerAst(), "Expected trigger abstract syntax tree for task " << task->absNodePath());
    BOOST_REQUIRE_MESSAGE(!task->triggerAst()->evaluate(), "Trigger should not evaluate");

    const Calendar& cal = suite->calendar();
    for (const ecf::TimeAttr& attr : task->timeVec()) {
        BOOST_CHECK_MESSAGE(!attr.isFree(cal), " Time should not be free");
    }
    for (const ecf::TodayAttr& attr : task->todayVec()) {
        BOOST_CHECK_MESSAGE(!attr.isFree(cal), " Today should not be free");
    }
    for (const ecf::CronAttr& attr : task->crons()) {
        BOOST_CHECK_MESSAGE(!attr.isFree(cal), " Cron should not be free");
    }
    for (const DateAttr& attr : task->dates()) {
        BOOST_CHECK_MESSAGE(!attr.isFree(cal), " Date should not be free");
    }
    for (const DayAttr& attr : task->days()) {
        BOOST_CHECK_MESSAGE(!attr.isFree(cal), "Day should not be free");
    }

    // Invoke the FreeDepCmd
    MockServer mockServer(&theDefs);

    FreeDepCmd cmd(task->absNodePath(), true /*trigger*/, true /* all */);
    cmd.setup_user_authentification();
    STC_Cmd_ptr returnCmd = cmd.handleRequest(&mockServer);
    BOOST_REQUIRE_MESSAGE(returnCmd->ok(), "Failed to for FreeDepCmd");

    // Dependencies should now all be free now.
    BOOST_REQUIRE_MESSAGE(task->get_trigger()->isFree(), "Trigger should evaluate");
    bool at_least_one_free = false;
    for (const ecf::TimeAttr& attr : task->timeVec()) {
        if (attr.isFree(cal)) {
            at_least_one_free = true;
            break;
        }
    }
    BOOST_CHECK_MESSAGE(at_least_one_free, "At least one Time should be free");

    at_least_one_free = false;
    for (const ecf::TodayAttr& attr : task->todayVec()) {
        if (attr.isFree(cal)) {
            at_least_one_free = true;
            break;
        }
    }
    BOOST_CHECK_MESSAGE(at_least_one_free, "At least one Today should be free");

    at_least_one_free = false;
    for (const ecf::CronAttr& attr : task->crons()) {
        if (attr.isFree(cal)) {
            at_least_one_free = true;
            break;
        }
    }
    BOOST_CHECK_MESSAGE(at_least_one_free, "At least one Cron should be free");

    at_least_one_free = false;
    for (const DateAttr& attr : task->dates()) {
        if (attr.isFree(cal)) {
            at_least_one_free = true;
            break;
        }
    }
    BOOST_CHECK_MESSAGE(at_least_one_free, "At least one Date should be free");

    at_least_one_free = false;
    for (const DayAttr& attr : task->days()) {
        if (attr.isFree(cal)) {
            at_least_one_free = true;
            break;
        }
    }
    BOOST_CHECK_MESSAGE(at_least_one_free, "At least one Day should be free");
}

BOOST_AUTO_TEST_CASE(test_free_dep_cmd_single_time_slot) {
    cout << "Base:: ...test_free_dep_cmd_single_time_slot\n";
    TestLog test_log("test_free_dep_cmd_single_time_slot.log"); // will create log file, and destroy log and remove file
                                                                // at end of scope

    // We add a time dependency *AFTER* the suite/calendar time
    // This checks that we DO NOT re-queue a task with a future time dependency
    // when a time dependency has been freed
    ptime theLocalTime(date(2011, Nov, 4), time_duration(9, 30, 0));
    ptime time_plus_2minute = theLocalTime + minutes(2);

    suite_ptr suite;
    task_ptr task;
    Defs theDefs;
    {
        suite = theDefs.add_suite("test_time");
        ClockAttr clockAttr(theLocalTime, false /*false means use real clock*/);
        suite->addClock(clockAttr);
        task = suite->add_task("t");
        task->addTime(ecf::TimeAttr(TimeSlot(time_plus_2minute.time_of_day())));
    }

    // This will initialise the calendar from the Clock attribute
    theDefs.beginAll();

    // expect task to be holding at 9:30
    const Calendar& cal = suite->calendar();
    for (const ecf::TimeAttr& attr : task->timeVec()) {
        BOOST_CHECK_MESSAGE(!attr.isFree(cal), " Time should not be free");
    }

    // Invoke the FreeDepCmd
    MockServer mockServer(&theDefs);
    FreeDepCmd cmd(task->absNodePath(), true /*trigger*/, true /* all */);
    cmd.setup_user_authentification();
    STC_Cmd_ptr returnCmd = cmd.handleRequest(&mockServer);
    BOOST_REQUIRE_MESSAGE(returnCmd->ok(), "Failed to for FreeDepCmd");

    // expect task to be free
    bool at_least_one_free = false;
    for (const ecf::TimeAttr& attr : task->timeVec()) {
        if (attr.isFree(cal)) {
            at_least_one_free = true;
            break;
        }
    }
    BOOST_CHECK_MESSAGE(at_least_one_free, "At least one Time should be free");

    // The crux of the test. Should not requeue.
    BOOST_CHECK_MESSAGE(task->state() == NState::ACTIVE,
                        "Free dependency should have done an immediate job submission. Expected active state");
    task->complete();
    BOOST_CHECK_MESSAGE(task->state() == NState::COMPLETE,
                        "Expected Complete but found "
                            << NState::toString(task->state())
                            << ", free dependency should avoid re-queue for a future single time dependency.");
}

BOOST_AUTO_TEST_CASE(test_free_dep_cmd_with_time_series) {
    cout << "Base:: ...test_free_dep_cmd_with_time_series\n";
    TestLog test_log("test_free_dep_cmd_with_time_series.log"); // will create log file, and destroy log and remove file
                                                                // at end of scope

    // We start the suite *IN BETWEEN* a time series, and the force free dependency
    // This checks that we DO NOT re-queue a task with a future time dependency
    // when a time dependency has been freed
    ptime theLocalTime(date(2011, Nov, 4), time_duration(9, 29, 0));
    suite_ptr suite;
    task_ptr task;
    Defs theDefs;
    {
        suite = theDefs.add_suite("test_time");
        ClockAttr clockAttr(theLocalTime, false /*false means use real clock*/);
        suite->addClock(clockAttr);
        task = suite->add_task("t");
        task->addTime(ecf::TimeAttr(
            ecf::TimeSlot(9, 28), ecf::TimeSlot(9, 30), ecf::TimeSlot(0, 2), false /*relative to suite start*/
            ));
    }

    // This will initialise the calendar from the Clock attribute
    theDefs.beginAll();

    // expect task to be holding, at 9:29
    const Calendar& cal = suite->calendar();
    for (const ecf::TimeAttr& attr : task->timeVec()) {
        BOOST_CHECK_MESSAGE(!attr.isFree(cal), " Time should not be free");
    }

    // Invoke the FreeDepCmd
    MockServer mockServer(&theDefs);
    FreeDepCmd cmd(task->absNodePath(), true /*trigger*/, true /* all */);
    cmd.setup_user_authentification();
    STC_Cmd_ptr returnCmd = cmd.handleRequest(&mockServer);
    BOOST_REQUIRE_MESSAGE(returnCmd->ok(), "Failed to for FreeDepCmd");

    // expect task to be free
    bool at_least_one_free = false;
    for (const ecf::TimeAttr& attr : task->timeVec()) {
        if (attr.isFree(cal)) {
            at_least_one_free = true;
            break;
        }
    }
    BOOST_CHECK_MESSAGE(at_least_one_free, "At least one Time should be free");

    // The crux of the test. Should not requeue, as we should have advanced the time slot beyond the END point
    // i.e FreeDepCmd will advance the time slot, beyond end time
    BOOST_CHECK_MESSAGE(task->state() == NState::ACTIVE,
                        "Free dependency should have done an immediate job submission. Expected active state");
    task->complete();
    BOOST_CHECK_MESSAGE(task->state() == NState::COMPLETE,
                        "Expected Complete but found "
                            << NState::toString(task->state())
                            << ", free dependency should avoid re-queue for a future single time dependency.");
}

BOOST_AUTO_TEST_CASE(test_free_dep_cmd_with_time_series_2) {
    cout << "Base:: ...test_free_dep_cmd_with_time_series_2\n";
    TestLog test_log("test_free_dep_cmd_with_time_series_2.log"); // will create log file, and destroy log and remove
                                                                  // file at end of scope

    // We start the suite *IN BETWEEN* a time series, and the force free dependency
    // This time we have a larger range, and *SHOULD* re-queue, even if we miss a time slot
    ptime theLocalTime(date(2011, Nov, 4), time_duration(9, 29, 0));
    suite_ptr suite;
    task_ptr task;
    Defs theDefs;
    {
        suite = theDefs.add_suite("test_time");
        ClockAttr clockAttr(theLocalTime, false /*false means use real clock*/);
        suite->addClock(clockAttr);
        task = suite->add_task("t");
        task->addTime(ecf::TimeAttr(
            ecf::TimeSlot(9, 28), ecf::TimeSlot(9, 34), ecf::TimeSlot(0, 2), false /*relative to suite start*/
            ));
    }

    // This will initialise the calendar from the Clock attribute
    // Will initialise  nextTimeSlot=09:30
    theDefs.beginAll();

    // expect task to be holding, at 9:29
    const Calendar& cal = suite->calendar();
    for (const ecf::TimeAttr& attr : task->timeVec()) {
        BOOST_CHECK_MESSAGE(!attr.isFree(cal), " Time should not be free");
    }

    // Invoke the FreeDepCmd. This will update next nextTimeSlot to 09:32
    MockServer mockServer(&theDefs);
    FreeDepCmd cmd(task->absNodePath(), true /*trigger*/, true /* all */);
    cmd.setup_user_authentification();
    STC_Cmd_ptr returnCmd = cmd.handleRequest(&mockServer);
    BOOST_REQUIRE_MESSAGE(returnCmd->ok(), "Failed to for FreeDepCmd");

    // expect task to be free
    bool at_least_one_free = false;
    for (const ecf::TimeAttr& attr : task->timeVec()) {
        if (attr.isFree(cal)) {
            at_least_one_free = true;
            break;
        }
    }
    BOOST_CHECK_MESSAGE(at_least_one_free, "At least one Time should be free");

    // The crux of the test. Even if we advance the time slot we should *STILL* be in range
    BOOST_CHECK_MESSAGE(task->state() == NState::ACTIVE,
                        "Free dependency should have done an immediate job submission. Expected active state");
    task->complete();
    BOOST_CHECK_MESSAGE(task->state() == NState::QUEUED,
                        "Expected QUEUED but found "
                            << NState::toString(task->state())
                            << ", free dependency should avoid re-queue for a future single time dependency.");
}

BOOST_AUTO_TEST_SUITE_END()

BOOST_AUTO_TEST_SUITE_END()
