/*
 * SPDX-FileCopyrightText: 2025 Copyright contributors to the cppTango project
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */
#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;
using CallbackMockType = TangoTest::CallbackMock<Tango::EventData>;
using CallbackMockItfcType = TangoTest::CallbackMock<Tango::DevIntrChangeEventData>;

const std::vector<std::string> static_commands = {"Init", "Status", "State", "PushChangeEvent"};
const std::vector<std::string> static_attributes = {
    "Status", "State", "Short_attr", "Slow_attr", "throw_read_attr", "no_change_event_attr"};

} // anonymous namespace

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

    void init_device() override { }

    void push_change_event(Tango::DevString attr)
    {
        if(!strcmp(attr, "Short_attr"))
        {
            Base::push_change_event(attr, &short_value);
        }
        else if(!strcmp(attr, "throw_read_attr"))
        {
            Base::push_change_event(attr, &double_value);
        }
        else
        {
            TANGO_THROW_EXCEPTION(TestExceptReason, "This is a test");
        }
    }

    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<&EventSubModeDevice::read_attribute>("Short_attr", Tango::DEV_SHORT);
            short_attr->set_change_event(true, false);
            attrs.push_back(short_attr);
        }

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

        {
            auto no_change_event =
                new TangoTest::AutoAttr<&EventSubModeDevice::read_attribute>("no_change_event_attr", Tango::DEV_DOUBLE);
            no_change_event->set_change_event(false, false);
            attrs.push_back(no_change_event);
        }

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

    static void command_factory(std::vector<Tango::Command *> &cmds)
    {
        cmds.push_back(new TangoTest::AutoCommand<&EventSubModeDevice::push_change_event>("PushChangeEvent"));
    }

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

TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(EventSubModeDevice, 4)

SCENARIO("Event subscriptions with EventSubMode works with plain attribute")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        std::string attr_name{"Short_attr"};

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

        WHEN("we subscribe to the attribute " << attr_name << " supporting change event using "
                                              << to_string(event_sub_mode))
        {
            CallbackMockType cb;

            TangoTest::Subscription sub{device, attr_name, Tango::CHANGE_EVENT, &cb, event_sub_mode};

            require_initial_events(cb, k_inital_short, event_sub_mode);

            THEN("fire a change event")
            {
                using namespace Catch::Matchers;
                using namespace TangoTest::Matchers;

                Tango::DeviceData in;
                in << attr_name;
                REQUIRE_NOTHROW(device->command_inout("PushChangeEvent", in));

                auto event = cb.pop_next_event();
                REQUIRE(event != std::nullopt);
                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::Update));
            }
        }
    }
}

SCENARIO("Event subscriptions without reading and throwing attr read function")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        std::string attr_name{"throw_read_attr"};

        // we now only subscribe without reading the attribute
        auto event_sub_mode = GENERATE(Tango::EventSubMode::Sync, Tango::EventSubMode::Async);

        WHEN("we subscribe to the attribute " << attr_name << " supporting change event using "
                                              << to_string(event_sub_mode))
        {
            CallbackMockType cb;

            TangoTest::Subscription sub{device, attr_name, Tango::CHANGE_EVENT, &cb, event_sub_mode};

            if(event_sub_mode == Tango::EventSubMode::Async)
            {
                using namespace Catch::Matchers;
                using namespace TangoTest::Matchers;

                auto event = cb.pop_next_event();
                REQUIRE(event != std::nullopt);
                REQUIRE_THAT(event->errors, IsEmpty());
                REQUIRE_THAT(event, EventType(Tango::CHANGE_EVENT));
                REQUIRE_THAT(event->attr_name, AttrNameContains(attr_name));
                REQUIRE_THAT(event, EventReason(Tango::EventReason::SubSuccess));
            }
            else
            {
                auto event = cb.pop_next_event();
                REQUIRE(event == std::nullopt);
            }

            THEN("fire a change event")
            {
                using namespace Catch::Matchers;
                using namespace TangoTest::Matchers;

                Tango::DeviceData in;
                in << attr_name;
                REQUIRE_NOTHROW(device->command_inout("PushChangeEvent", in));

                auto event = cb.pop_next_event();
                REQUIRE(event != std::nullopt);
                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)));
                // and we get the pushed value, as there is no value which can be read
                REQUIRE_THAT(event, EventValueMatches(AnyLikeContains(k_inital_double)));
                REQUIRE_THAT(event, EventReason(Tango::EventReason::Update));
            }
        }
    }
}

SCENARIO("Event subscriptions with reading and throwing attr read function")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        std::string attr_name{"throw_read_attr"};

        auto event_sub_mode =
            GENERATE(Tango::EventSubMode::SyncRead, 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;

            CallbackMockType cb;

            TangoTest::Subscription sub{device, attr_name, Tango::CHANGE_EVENT, &cb, event_sub_mode};

            auto event = cb.pop_next_event();
            REQUIRE(event != std::nullopt);
            REQUIRE_THAT(event->errors, !IsEmpty() && AnyMatch(Reason(TestExceptReason)));
            REQUIRE_THAT(event, EventType(Tango::CHANGE_EVENT));
            REQUIRE_THAT(event, AttrNameContains(attr_name));
            REQUIRE_THAT(event, EventReason(Tango::EventReason::SubSuccess));

            THEN("fire a change event")
            {
                Tango::DeviceData in;
                in << attr_name;
                REQUIRE_NOTHROW(device->command_inout("PushChangeEvent", in));

                auto event = cb.pop_next_event();
                REQUIRE(event != std::nullopt);
                REQUIRE_THAT(event->errors, IsEmpty());
                REQUIRE_THAT(event, EventType(Tango::CHANGE_EVENT));
                REQUIRE_THAT(event, AttrNameContains(attr_name));
                REQUIRE_THAT(event, EventValueMatches(AttrQuality(Tango::ATTR_VALID)));
                // and we get the pushed value, as there is no value which can be read
                REQUIRE_THAT(event, EventValueMatches(AnyLikeContains(k_inital_double)));
                REQUIRE_THAT(event, EventReason(Tango::EventReason::Update));
            }
        }
    }
}

SCENARIO("Event subscriptions async with stopped DS")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();
        ctx.stop_server();

        std::string attr_name{"throw_read_attr"};

        auto event_sub_mode =
            GENERATE(Tango::EventSubMode::Async, 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;

            CallbackMockType cb;

            TangoTest::Subscription sub{device, attr_name, Tango::CHANGE_EVENT, &cb, event_sub_mode};

            auto event = cb.pop_next_event();
            REQUIRE(event != std::nullopt);
            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));

            ctx.restart_server();
            std::this_thread::sleep_for(std::chrono::seconds(2 * Tango::EVENT_HEARTBEAT_PERIOD + 1));

            AND_THEN("after restarting the server and waiting enough we get a subscription success event")
            {
                auto event = cb.pop_next_event();
                REQUIRE(event != std::nullopt);
                if(event_sub_mode == Tango::EventSubMode::AsyncRead || event_sub_mode == Tango::EventSubMode::Stateless)
                {
                    REQUIRE_THAT(event->errors, !IsEmpty() && AnyMatch(Reason(TestExceptReason)));
                }
                else
                {
                    REQUIRE_THAT(event->errors, IsEmpty());
                }

                REQUIRE_THAT(event, EventType(Tango::CHANGE_EVENT));
                REQUIRE_THAT(event, AttrNameContains(attr_name));
                REQUIRE_THAT(event, EventReason(Tango::EventReason::SubSuccess));

                AND_THEN("fire a change event")
                {
                    Tango::DeviceData in;
                    in << attr_name;
                    REQUIRE_NOTHROW(device->command_inout("PushChangeEvent", in));

                    auto event = cb.pop_next_event();
                    REQUIRE(event != std::nullopt);
                    REQUIRE_THAT(event->errors, IsEmpty());
                    REQUIRE_THAT(event, EventType(Tango::CHANGE_EVENT));
                    REQUIRE_THAT(event, EventValueMatches(AttrQuality(Tango::ATTR_VALID)));
                    REQUIRE_THAT(event, AttrNameContains(attr_name));
                    // and we get the pushed value, as there is no value which can be read
                    REQUIRE_THAT(event, EventValueMatches(AnyLikeContains(k_inital_double)));
                    REQUIRE_THAT(event, EventReason(Tango::EventReason::Update));
                }
            }
        }
    }
}

SCENARIO("Event subscriptions sync with stopped DS")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();
        ctx.stop_server();

        std::string attr_name{"throw_read_attr"};

        auto event_sub_mode = GENERATE(Tango::EventSubMode::Sync, Tango::EventSubMode::SyncRead);

        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;

            CallbackMockType cb;

            REQUIRE_THROWS_MATCHES(device->subscribe_event(attr_name, Tango::CHANGE_EVENT, &cb, event_sub_mode),
                                   Tango::DevFailed,
                                   FirstErrorMatches(Reason(Tango::API_CantConnectToDevice)));

            ctx.restart_server();
            std::this_thread::sleep_for(std::chrono::seconds(2 * Tango::EVENT_HEARTBEAT_PERIOD + 1));

            THEN("fire a change event")
            {
                Tango::DeviceData in;
                in << attr_name;
                REQUIRE_NOTHROW(device->command_inout("PushChangeEvent", in));

                // nothing as it's sync without stateless, so there no internal retry
                auto event = cb.pop_next_event();
                REQUIRE(event == std::nullopt);
            }
        }
    }
}

SCENARIO("Event subscriptions with interface change event (callback)")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_DEV_INTR));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

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

        WHEN("we subscribe to the interface change event using " << to_string(event_sub_mode))
        {
            using namespace Catch::Matchers;
            using namespace TangoTest::Matchers;

            CallbackMockItfcType cb;

            TangoTest::Subscription sub{device, Tango::INTERFACE_CHANGE_EVENT, &cb, event_sub_mode};

            THEN("we got an interface change event")
            {
                if(event_sub_mode == Tango::EventSubMode::Async)
                {
                    using namespace Catch::Matchers;
                    using namespace TangoTest::Matchers;

                    auto event = cb.pop_next_event();
                    REQUIRE(event != std::nullopt);
                    REQUIRE_THAT(event->errors, IsEmpty());
                    REQUIRE_THAT(event, EventDeviceStarted(false));
                    REQUIRE_THAT(event, EventType(Tango::INTERFACE_CHANGE_EVENT));
                    REQUIRE_THAT(event, EventCommandNamesMatches(IsEmpty()));
                    REQUIRE_THAT(event, EventAttributeNamesMatches(IsEmpty()));
                    REQUIRE_THAT(event, EventReason(Tango::EventReason::SubSuccess));
                }
                else if(event_sub_mode == Tango::EventSubMode::SyncRead ||
                        event_sub_mode == Tango::EventSubMode::AsyncRead ||
                        event_sub_mode == Tango::EventSubMode::Stateless)
                {
                    auto event = cb.pop_next_event();
                    REQUIRE(event != std::nullopt);
                    REQUIRE_THAT(event->errors, IsEmpty());
                    REQUIRE_THAT(event, EventDeviceStarted(true));
                    REQUIRE_THAT(event, EventType(Tango::INTERFACE_CHANGE_EVENT));
                    REQUIRE_THAT(event, EventCommandNamesMatches(UnorderedRangeEquals(static_commands)));
                    REQUIRE_THAT(event, EventAttributeNamesMatches(UnorderedRangeEquals(static_attributes)));
                    REQUIRE_THAT(event, EventReason(Tango::EventReason::SubSuccess));
                }
                else if(event_sub_mode == Tango::EventSubMode::Sync)
                {
                    auto event = cb.pop_next_event();
                    REQUIRE(event == std::nullopt);
                }
                else
                {
                    FAIL();
                }
            }
        }
    }
}

SCENARIO("Event subscriptions with interface change event (event queue)")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_DEV_INTR));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        auto event_sub_mode = GENERATE(Tango::EventSubMode::SyncRead);

        WHEN("we subscribe to the interface change event using " << to_string(event_sub_mode))
        {
            using namespace Catch::Matchers;
            using namespace TangoTest::Matchers;

            const int event_queue_size{10};
            TangoTest::Subscription sub{device, Tango::INTERFACE_CHANGE_EVENT, event_queue_size, event_sub_mode};

            THEN("we got an interface change event")
            {
                Tango::DevIntrChangeEventDataList list;
                device->get_events(sub.get_id(), list);
                REQUIRE_THAT(list, !IsEmpty());
                REQUIRE(list[0] != nullptr);
                auto event{*list[0]};
                REQUIRE_THAT(event.errors, IsEmpty());
                REQUIRE_THAT(event, EventDeviceStarted(true));
                REQUIRE_THAT(event, EventType(Tango::INTERFACE_CHANGE_EVENT));
                REQUIRE_THAT(event, EventCommandNamesMatches(UnorderedRangeEquals(static_commands)));
                REQUIRE_THAT(event, EventAttributeNamesMatches(UnorderedRangeEquals(static_attributes)));
                REQUIRE_THAT(event, EventReason(Tango::EventReason::SubSuccess));
            }
        }
    }
}

SCENARIO("Event subscriptions with AttributeProxy (callback)")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        std::string attr_name{"Short_attr"};

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

        WHEN("we subscribe to the attribute " << attr_name << " supporting change event using "
                                              << to_string(event_sub_mode))
        {
            CallbackMockType cb;
            auto ap = std::make_shared<Tango::AttributeProxy>(device.get(), attr_name);

            TangoTest::Subscription<Tango::AttributeProxy> sub{ap, Tango::CHANGE_EVENT, &cb, event_sub_mode};

            require_initial_events(cb, k_inital_short, event_sub_mode);

            THEN("fire a change event")
            {
                using namespace Catch::Matchers;
                using namespace TangoTest::Matchers;

                Tango::DeviceData in;
                in << attr_name;
                REQUIRE_NOTHROW(device->command_inout("PushChangeEvent", in));

                auto event = cb.pop_next_event();
                REQUIRE(event != std::nullopt);
                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::Update));
            }
        }
    }
}

SCENARIO("Event subscriptions with AttributeProxy (event queue)")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        std::string attr_name{"Short_attr"};

        auto event_sub_mode = GENERATE(Tango::EventSubMode::SyncRead);

        WHEN("we subscribe to the attribute " << attr_name << " supporting change event using "
                                              << to_string(event_sub_mode))
        {
            auto ap = std::make_shared<Tango::AttributeProxy>(device.get(), attr_name);

            const int event_queue_size{10};
            TangoTest::Subscription<Tango::AttributeProxy> sub{
                ap, Tango::CHANGE_EVENT, event_queue_size, event_sub_mode};

            THEN("fire a change event")
            {
                using namespace Catch::Matchers;
                using namespace TangoTest::Matchers;

                Tango::DeviceData in;
                in << attr_name;
                REQUIRE_NOTHROW(device->command_inout("PushChangeEvent", in));

                Tango::EventDataList list;
                device->get_events(sub.get_id(), 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)));
                // we are looking at the first event here which is SubSuccess
                REQUIRE_THAT(event, EventReason(Tango::EventReason::SubSuccess));
            }
        }
    }
}

SCENARIO("Event subscriptions throw with change events disabled")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        std::string attr_name{"no_change_event_attr"};

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

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

            CallbackMockType cb;

            auto subscribe = [&]() -> auto
            { return TangoTest::Subscription{device, attr_name, Tango::CHANGE_EVENT, &cb, event_sub_mode}; };

            if(event_sub_mode == Tango::EventSubMode::SyncRead || event_sub_mode == Tango::EventSubMode::Sync)
            {
                REQUIRE_THROWS_MATCHES(
                    subscribe(), Tango::DevFailed, FirstErrorMatches(Reason(Tango::API_AttributePollingNotStarted)));
            }
            else
            {
                auto sub = subscribe();

                auto event = cb.pop_next_event();
                REQUIRE(event != std::nullopt);
                REQUIRE_THAT(event->errors, !IsEmpty() && AnyMatch(Reason(Tango::API_AttributePollingNotStarted)));
                REQUIRE_THAT(event, EventType(Tango::CHANGE_EVENT));
                REQUIRE_THAT(event, AttrNameContains(attr_name));
                REQUIRE_THAT(event, EventReason(Tango::EventReason::SubFail));
            }
        }
    }
}

SCENARIO("Event subscriptions with async read and slow attribute (callback)")
{
    int idlver = GENERATE(TangoTest::idlversion(Tango::MIN_IDL_ZMQ_EVENT));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"esm", "EventSubModeDevice", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        std::string attr_name{"Slow_attr"};

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

        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;

            CallbackMockType cb0, cb1;

            auto before = Tango::make_TimeVal(std::chrono::system_clock::now());
            auto sub0 = TangoTest::Subscription{device, attr_name, Tango::CHANGE_EVENT, &cb0, event_sub_mode};

            // yield the current thread and allow the keep alive thread to kick in
            std::this_thread::sleep_for(100ms);

            auto sub1 = TangoTest::Subscription{device, attr_name, Tango::CHANGE_EVENT, &cb1, event_sub_mode};
            auto after = Tango::make_TimeVal(std::chrono::system_clock::now());

            // ensure that the time it takes until the subscribe_event calls return is less
            // than the time spent in read_attribute for Slow_attr
            REQUIRE_THAT(before, WithinTimeAbsMatcher(after, slow_attr_waiting * 0.9));

            {
                auto event = cb0.pop_next_event(slow_attr_waiting * 2);
                REQUIRE(event != std::nullopt);
                REQUIRE_THAT(event->errors, IsEmpty());
                REQUIRE_THAT(event, EventType(Tango::CHANGE_EVENT));
                REQUIRE_THAT(event, AttrNameContains(attr_name));
                REQUIRE_THAT(event, EventReason(Tango::EventReason::SubSuccess));
                REQUIRE_THAT(event, EventValueMatches(AnyLikeContains(k_inital_double)));
            }

            {
                auto event = cb1.pop_next_event(slow_attr_waiting * 2);
                REQUIRE(event != std::nullopt);
                REQUIRE_THAT(event->errors, IsEmpty());
                REQUIRE_THAT(event, EventType(Tango::CHANGE_EVENT));
                REQUIRE_THAT(event, AttrNameContains(attr_name));
                REQUIRE_THAT(event, EventReason(Tango::EventReason::SubSuccess));
                REQUIRE_THAT(event, EventValueMatches(AnyLikeContains(k_inital_double)));
            }
        }
    }
}
