#include "hsm/hsm.h"

#include <boost/hana.hpp>
#include <boost/hana/experimental/printable.hpp>
#include <gtest/gtest.h>

#include <future>
#include <memory>

namespace {

using namespace ::testing;
using namespace boost::hana;

// States
struct S1 {
    std::string name = "S1";
};
struct S2 {
    S2(std::string name)
        : name(name)
    {
    }
    const std::string name;
};

// Events
struct e1 {
    std::string name = "e1";
};
struct e2 {
};
struct e3 {
};
struct e4 {
    std::string name = "e4";
};

/*
 * A state factory is functor that constructs a non default constructible target state
 *
 * @param[in] event event of the transition
 * @param[in] source source state of the transition
 *
 * @return unique_ptr of the target state
 */
const auto stateFactory
    = [](auto event, auto source) { return std::make_unique<S2>(event.name + source.name); };

const auto alternativeStateFactory = [](auto event, auto source) {
    return std::make_unique<S2>(std::string { "static name" } + event.name + source.name);
};

const auto log = [](auto event, auto source, auto target) {
    std::cout << experimental::print(typeid_(source)) << ".name=" << source.name << " + "
              << experimental::print(typeid_(event)) << " = "
              << experimental::print(typeid_(target)) << ".name=" << target.name << std::endl;
};

struct MainState {
    static constexpr auto make_transition_table()
    {
        // clang-format off
        return hsm::transition_table(
            // Transitions with a non default constructible target need to create the state with
            // the create_state action and provide a state factory functor
            * hsm::state<S1> + hsm::event<e1> / hsm::create_state(stateFactory)            = hsm::state<S2>
            , hsm::state<S1> + hsm::event<e4> / hsm::create_state(alternativeStateFactory) = hsm::state<S2> 
            // If no create_state action is used the target state should already be created
            , hsm::state<S1> + hsm::event<e3>                                              = hsm::state<S2>
            // If you want add an action to the transition you need to encapsulate the action
            // into the reuse_state action
            , hsm::state<S1> + hsm::event<e2> / hsm::reuse_state(log)                      = hsm::state<S2>
            , hsm::state<S2> + hsm::event<e2> / log                                        = hsm::state<S1>
        );
        // clang-format on
    }

    static constexpr auto on_unexpected_event()
    {
        return [](auto event, auto /*currentState*/) {
            throw std::runtime_error(
                std::string("unexpected event ") + experimental::print(typeid_(event)));
        };
    }
};
}

class CustomTargetsTests : public Test {
  protected:
    hsm::sm<MainState> sm;
};

TEST_F(CustomTargetsTests, should_construct_state_on_transition)
{
    ASSERT_TRUE(sm.is(hsm::state<S1>));
    sm.process_event(e1 {});
    ASSERT_TRUE(sm.is(hsm::state<S2>));
}

TEST_F(CustomTargetsTests, should_construct_state_on_transition_with_alternative_state_factory)
{
    ASSERT_TRUE(sm.is(hsm::state<S1>));
    sm.process_event(e4 {});
    ASSERT_TRUE(sm.is(hsm::state<S2>));
}

TEST_F(CustomTargetsTests, should_reuse_constructed_state)
{
    ASSERT_TRUE(sm.is(hsm::state<S1>));
    sm.process_event(e1 {});
    sm.process_event(e2 {});
    sm.process_event(e2 {});
    ASSERT_TRUE(sm.is(hsm::state<S2>));
    sm.process_event(e2 {});
    ASSERT_TRUE(sm.is(hsm::state<S1>));
    sm.process_event(e3 {});
    ASSERT_TRUE(sm.is(hsm::state<S2>));
}