/*
 * SPDX-FileCopyrightText: 2025 Copyright contributors to the cppTango project
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */
#include "catch2_common.h"

#include <tango/internal/stl_corba_helpers.h>

namespace
{
const std::string TestExceptReason = "Ahhh!";
using CallbackMockType = TangoTest::CallbackMock<Tango::DataReadyEventData>;

} // anonymous namespace

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

    void init_device() override { }

    void push_data_ready(const Tango::DevVarLongStringArray &in)
    {
        Base::push_data_ready_event(in.svalue[0].in(), in.lvalue[0]);
    }

    void read_attribute(Tango::Attribute &att)
    {
        if(att.get_name() == "Long_attr")
        {
            att.set_value(&long_value);
        }
        else if(att.get_name() == "Short_attr")
        {
            att.set_value(&short_value);
        }
        else
        {
            TANGO_THROW_EXCEPTION(TestExceptReason, "This is a test");
        }
    }

    static void attribute_factory(std::vector<Tango::Attr *> &attrs)
    {
        {
            auto long_attr = new TangoTest::AutoAttr<&DataReadyDev::read_attribute>("Long_attr", Tango::DEV_LONG);
            attrs.push_back(long_attr);
        }

        {
            auto short_attr = new TangoTest::AutoAttr<&DataReadyDev::read_attribute>("Short_attr", Tango::DEV_SHORT);
            Tango::UserDefaultAttrProp props;
            short_attr->set_default_properties(props);
            short_attr->set_data_ready_event(true);
            attrs.push_back(short_attr);
        }

        {
            auto throw_attr = new TangoTest::AutoAttr<&DataReadyDev::read_attribute>("throw_attr", Tango::DEV_DOUBLE);
            Tango::UserDefaultAttrProp props;
            throw_attr->set_default_properties(props);
            throw_attr->set_data_ready_event(true);
            attrs.push_back(throw_attr);
        }
    }

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

  private:
    Tango::DevLong long_value{0};
    Tango::DevShort short_value{0};
};

TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(DataReadyDev, 1)

SCENARIO("DataReadyEvent failure")
{
    int idlver = GENERATE(TangoTest::idlversion(1));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"dr", "DataReadyDev", idlver};
        auto device = ctx.get_proxy();

        std::string attr_name{"long_attr"};

        WHEN("we subscribe to the attribute " << attr_name << " without data ready event enabled")
        {
            using namespace TangoTest::Matchers;

            CallbackMockType cb;
            REQUIRE_THROWS_MATCHES(device->subscribe_event(attr_name, Tango::DATA_READY_EVENT, &cb),
                                   Tango::DevFailed,
                                   FirstErrorMatches(Reason(Tango::API_AttributeNotDataReadyEnabled)));
        }
    }
}

SCENARIO("DataReadyEvent works (callback)")
{
    // DataReadyEvent is only supported for IDLv4 and above
    int idlver = GENERATE(TangoTest::idlversion(4));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"dr", "DataReadyDev", 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 data ready event")
        {
            CallbackMockType cb;

            TangoTest::Subscription sub1{device, attr_name, Tango::DATA_READY_EVENT, &cb};
            TangoTest::Subscription sub2{device, attr_name, Tango::DATA_READY_EVENT, &cb};

            THEN("check that the attribute is still not polled")
            {
                REQUIRE(!device->is_attribute_polled(attr_name));
            }

            THEN("the callback should not have been executed once")
            {
                auto event = cb.pop_next_event();
                REQUIRE(event == std::nullopt);
            }

            THEN("fire a data ready event")
            {
                using namespace TangoTest::Matchers;
                using namespace std::literals::chrono_literals;

                const int counter = 10;

                Tango::DevVarLongStringArray dvlsa;
                dvlsa.svalue.length(1);
                dvlsa.lvalue.length(1);
                dvlsa.lvalue[0] = counter;
                dvlsa.svalue[0] = Tango::string_dup(attr_name.c_str());
                Tango::DeviceData in;
                in << dvlsa;

                REQUIRE_NOTHROW(device->command_inout("PushDataReady", in));
                auto now = Tango::make_TimeVal(std::chrono::system_clock::now());

                // we only want to check if the date is set, so we use a large margin
                auto margin = 1s;

                auto event1 = cb.pop_next_event();
                REQUIRE(event1 != std::nullopt);
                REQUIRE_THAT(event1, EventType(Tango::DATA_READY_EVENT));
                REQUIRE_THAT(event1, EventCounter(counter));
                REQUIRE_THAT(event1, EventAttrType(Tango::DEV_SHORT));
                REQUIRE_THAT(event1->get_date(), WithinTimeAbsMatcher(now, margin));

                auto event2 = cb.pop_next_event();
                REQUIRE(event2 != std::nullopt);
                REQUIRE_THAT(event2, EventType(Tango::DATA_READY_EVENT));
                REQUIRE_THAT(event2, EventCounter(counter));
                REQUIRE_THAT(event2, EventAttrType(Tango::DEV_SHORT));
                REQUIRE_THAT(event2->get_date(), WithinTimeAbsMatcher(now, margin));
            }

            THEN("push a couple more events")
            {
                using namespace Catch::Matchers;
                using namespace TangoTest::Matchers;

                Tango::DevVarLongStringArray dvlsa;
                dvlsa.svalue.length(1);
                dvlsa.lvalue.length(1);
                dvlsa.svalue[0] = Tango::string_dup(attr_name.c_str());
                Tango::DeviceData in;

                dvlsa.lvalue[0] = 15;
                in << dvlsa;
                device->command_inout("PushDataReady", in);

                dvlsa.lvalue[0] = 16;
                in << dvlsa;
                device->command_inout("PushDataReady", in);

                dvlsa.lvalue[0] = 17;
                in << dvlsa;
                device->command_inout("PushDataReady", in);

                auto events = cb.pop_events();

                REQUIRE_THAT(events, SizeIs(6));

                auto event = events.back();
                REQUIRE(event != std::nullopt);
                REQUIRE_THAT(event, EventType(Tango::DATA_READY_EVENT));
                REQUIRE_THAT(event, EventCounter(17));
                REQUIRE_THAT(event, EventAttrType(Tango::DEV_SHORT));
            }
        }
    }
}

SCENARIO("push_data_ready_event on non-existing attribute")
{
    int idlver = GENERATE(TangoTest::idlversion(1));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"dr", "DataReadyDev", idlver};
        auto device = ctx.get_proxy();

        WHEN("fire a data ready event")
        {
            using namespace Catch::Matchers;
            using namespace TangoTest::Matchers;

            Tango::DevVarLongStringArray dvlsa;
            dvlsa.svalue.length(1);
            dvlsa.lvalue.length(1);
            dvlsa.svalue[0] = Tango::string_dup("bidon");
            Tango::DeviceData in;
            in << dvlsa;

            REQUIRE_THROWS_MATCHES(device->command_inout("PushDataReady", in),
                                   Tango::DevFailed,
                                   FirstErrorMatches(Reason(Tango::API_AttrNotFound)));
        }
    }
}

SCENARIO("push_data_ready_event on throwing attribute read function")
{
    int idlver = GENERATE(TangoTest::idlversion(6));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"dr", "DataReadyDev", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        std::string attr_name{"throw_attr"};

        WHEN("we subscribe to the attribute " << attr_name << " supporting data ready event")
        {
            CallbackMockType cb;
            TangoTest::Subscription sub{device, attr_name, Tango::DATA_READY_EVENT, &cb};

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

                const int counter = 4711;

                Tango::DevVarLongStringArray dvlsa;
                dvlsa.svalue.length(1);
                dvlsa.lvalue.length(1);
                dvlsa.svalue[0] = Tango::string_dup(attr_name.c_str());
                dvlsa.lvalue[0] = counter;
                Tango::DeviceData in;
                in << dvlsa;

                REQUIRE_NOTHROW(device->command_inout("PushDataReady", in));

                // data ready event does not read the attribute but only provides a TLR to it in attr_name
                // so it does not matter here that the read function throws.
                auto event = cb.pop_next_event();
                REQUIRE(event != std::nullopt);
                REQUIRE_THAT(event, EventType(Tango::DATA_READY_EVENT));
                REQUIRE_THAT(event, EventCounter(counter));
                REQUIRE_THAT(event, EventAttrType(Tango::DEV_DOUBLE));
                REQUIRE_THAT(event->errors, IsEmpty());

                // we never get an exception from the data ready event when fetching the result as
                // EventSupplier::push_att_data_ready_event always passes a null pointer for except to
                // ZmqEventSupplier::push_event
            }
        }
    }
}

SCENARIO("DataReadyEvent works (event queue)")
{
    int idlver = GENERATE(TangoTest::idlversion(4));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"dr", "DataReadyDev", 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 data ready event")
        {
            std::vector<std::string> filters;
            TangoTest::Subscription sub{device, attr_name, Tango::DATA_READY_EVENT, 0, filters};

            THEN("check that the attribute is still not polled")
            {
                REQUIRE(!device->is_attribute_polled(attr_name));
            }

            THEN("no events yet")
            {
                REQUIRE(device->is_event_queue_empty(sub.get_id()));
            }

            THEN("fire data ready events")
            {
                using namespace Catch::Matchers;
                using namespace TangoTest::Matchers;
                using namespace std::literals::chrono_literals;

                const int counter = 9;
                const int num_pushes = counter + 1;

                Tango::DevVarLongStringArray dvlsa;
                dvlsa.svalue.length(1);
                dvlsa.lvalue.length(1);
                dvlsa.lvalue[0] = 0;
                dvlsa.svalue[0] = Tango::string_dup(attr_name.c_str());
                Tango::DeviceData in;

                for(int i = 0; i < num_pushes; i++)
                {
                    dvlsa.lvalue[0] = i;
                    in << dvlsa;
                    REQUIRE_NOTHROW(device->command_inout("PushDataReady", in));
                }

                auto now = Tango::make_TimeVal(std::chrono::system_clock::now());
                // we only want to check if the date is set, so we use a large margin
                auto margin = 1s;

                // wait until we got all events
                for(size_t i = 0; i < 1000; i++)
                {
                    if(device->event_queue_size(sub.get_id()) == num_pushes)
                    {
                        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(sub.get_id()) == num_pushes);

                CallbackMockType cb;
                device->get_events(sub.get_id(), &cb);

                auto events = cb.pop_events();
                REQUIRE_THAT(events, SizeIs(num_pushes));
                auto event = events.back();
                REQUIRE(event != std::nullopt);
                REQUIRE_THAT(event->errors, IsEmpty());
                REQUIRE_THAT(event, EventType(Tango::DATA_READY_EVENT));
                REQUIRE_THAT(event, EventCounter(counter));
                REQUIRE_THAT(event, EventAttrType(Tango::DEV_SHORT));
                REQUIRE_THAT(event->get_date(), WithinTimeAbsMatcher(now, margin));

                // no more events
                event = cb.pop_next_event();
                REQUIRE(event == std::nullopt);
            }
        }
    }
}
