#include "catch2_common.h"

#include <tango/internal/utils.h>

namespace
{
using namespace std::literals::chrono_literals;

const std::string TestExceptReason = "Ahhh!";
const Tango::DevShort k_inital_short = 5678;
const Tango::DevDouble k_inital_double = 47.11;
const auto slow_attr_waiting = 2s;
} // namespace

template <class Base>
class EventQueueDevice : public Base
{
  public:
    using Base::Base;

    void init_device() override { }

    void read_attribute(Tango::Attribute &att)
    {
        if(att.get_name() == "Short_attr")
        {
            att.set_value(&short_value);
        }
        else if(att.get_name() == "Slow_attr")
        {
            std::this_thread::sleep_for(slow_attr_waiting);
            att.set_value(&slow_attr_value);
        }
        else
        {
            TANGO_THROW_EXCEPTION(TestExceptReason, "This is a test");
        }
    }

    static void attribute_factory(std::vector<Tango::Attr *> &attrs)
    {
        {
            auto short_attr =
                new TangoTest::AutoAttr<&EventQueueDevice::read_attribute>("Short_attr", Tango::DEV_SHORT);
            short_attr->set_change_event(true, false);
            attrs.push_back(short_attr);
        }

        {
            auto slow_attr = new TangoTest::AutoAttr<&EventQueueDevice::read_attribute>("Slow_attr", Tango::DEV_DOUBLE);
            slow_attr->set_change_event(true, false);
            attrs.push_back(slow_attr);
        }
    }

  private:
    Tango::DevShort short_value{k_inital_short};
    Tango::DevDouble slow_attr_value{k_inital_double};
};

TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(EventQueueDevice, 4)

SCENARIO("Event queue works")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"eqd", "EventQueueDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        std::string attr_name{"Short_attr"};

        WHEN("we subscribe to the attribute " << attr_name << " supporting change event")
        {
            const int event_queue_size{1};
            TangoTest::Subscription sub{
                device, attr_name, Tango::CHANGE_EVENT, event_queue_size, Tango::EventSubMode::SyncRead};

            THEN("we got a change event")
            {
                using namespace Catch::Matchers;
                using namespace TangoTest::Matchers;
                using namespace std::literals::chrono_literals;

                auto eid = sub.get_id();

                REQUIRE(device->event_queue_size(eid) == 1);
                REQUIRE(!device->is_event_queue_empty(eid));

                auto tv = device->get_last_event_date(eid);

                Tango::EventDataList list;
                device->get_events(eid, list);

                REQUIRE_THAT(list, !IsEmpty());
                REQUIRE(list[0] != nullptr);
                auto event{*list[0]};
                REQUIRE_THAT(event.errors, IsEmpty());
                REQUIRE_THAT(event, EventType(Tango::CHANGE_EVENT));
                REQUIRE_THAT(event, EventValueMatches(AttrQuality(Tango::ATTR_VALID)));
                REQUIRE_THAT(event, EventValueMatches(AttrNameContains(attr_name)));
                REQUIRE_THAT(event, EventValueMatches(AnyLikeContains(k_inital_short)));
                REQUIRE_THAT(event, EventReason(Tango::EventReason::SubSuccess));
                REQUIRE_THAT(event.get_date(), WithinTimeAbsMatcher(tv, 0ns));

                // and now it is empty
                REQUIRE(device->event_queue_size(eid) == 0);
                REQUIRE(device->is_event_queue_empty(eid));

                REQUIRE_THROWS_MATCHES(
                    device->get_last_event_date(eid),
                    Tango::DevFailed,
                    FirstErrorMatches(Reason(Tango::API_EventQueues) &&
                                      DescriptionMatches(ContainsSubstring("No new events available!"))));

                device->get_events(eid, list);
                REQUIRE_THAT(list, IsEmpty());
            }
        }
    }
}

SCENARIO("Event queue throws with querying unknown event id")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"eqd", "EventQueueDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        const int eid = 4711;

        AND_GIVEN("an event id " << eid << " which was not subscribed")
        {
            using namespace Catch::Matchers;
            using namespace TangoTest::Matchers;

            const std::string fail_msg = "event id specified does not correspond with any known one";

            REQUIRE_THROWS_MATCHES(
                device->event_queue_size(eid),
                Tango::DevFailed,
                FirstErrorMatches(Reason(Tango::API_EventNotFound) && DescriptionMatches(ContainsSubstring(fail_msg))));

            REQUIRE_THROWS_MATCHES(
                device->is_event_queue_empty(eid),
                Tango::DevFailed,
                FirstErrorMatches(Reason(Tango::API_EventNotFound) && DescriptionMatches(ContainsSubstring(fail_msg))));

            REQUIRE_THROWS_MATCHES(
                device->get_last_event_date(eid),
                Tango::DevFailed,
                FirstErrorMatches(Reason(Tango::API_EventNotFound) && DescriptionMatches(ContainsSubstring(fail_msg))));

            Tango::EventDataList list;
            REQUIRE_THROWS_MATCHES(
                device->get_events(eid, list),
                Tango::DevFailed,
                FirstErrorMatches(Reason(Tango::API_EventNotFound) && DescriptionMatches(ContainsSubstring(fail_msg))));
        }
    }
}

SCENARIO("Event queue from unconnected events can be queried")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"eqd", "EventQueueDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();
        ctx.stop_server();

        std::string attr_name{"Slow_attr"};

        auto event_sub_mode = GENERATE(Tango::EventSubMode::AsyncRead, Tango::EventSubMode::Stateless);

        WHEN("we subscribe to the attribute " << attr_name << " supporting change event using "
                                              << to_string(event_sub_mode))
        {
            using namespace Catch::Matchers;
            using namespace TangoTest::Matchers;
            using namespace std::literals::chrono_literals;

            const int event_queue_size{0};
            auto sub =
                TangoTest::Subscription{device, attr_name, Tango::CHANGE_EVENT, event_queue_size, event_sub_mode};

            auto eid = sub.get_id();

            // wait until we got all events
            for(size_t i = 0; i < 1000; i++)
            {
                if(device->event_queue_size(eid) == 1)
                {
                    break;
                }

                // wait a bit so that we can be sure all events are processed
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
            }

            REQUIRE(device->event_queue_size(eid) == 1);
            REQUIRE(!device->is_event_queue_empty(eid));

            auto tv = device->get_last_event_date(eid);

            Tango::EventDataList list;
            device->get_events(eid, list);

            REQUIRE_THAT(list, !IsEmpty());
            REQUIRE(list[0] != nullptr);
            auto event{*list[0]};
            REQUIRE_THAT(event.errors, !IsEmpty() && AnyMatch(Reason(Tango::API_CantConnectToDevice)));
            REQUIRE_THAT(event, EventType(Tango::CHANGE_EVENT));
            REQUIRE_THAT(event, AttrNameContains(attr_name));
            REQUIRE_THAT(event, EventReason(Tango::EventReason::SubFail));
            REQUIRE_THAT(event.get_date(), WithinTimeAbsMatcher(tv, 0ns));
        }
    }
}
