#include "catch2_common.h"

static constexpr double k_initial_value{1.1234};

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

    void init_device() override { }

    void read_attr(Tango::Attribute &att) override
    {
        att.set_value(&attr_dq_double);
    }

    void write_attr(Tango::WAttribute &att)
    {
        att.get_write_value(attr_dq_double);
    }

    void set_attribute_limit(const Tango::DevVarDoubleArray &in)
    {
        if(in.length() != 2)
        {
            TANGO_THROW_EXCEPTION("Invalid data", "Expected two elements");
        }

        Tango::WAttribute &w_attr = this->dev_attr->get_w_attr_by_name("double_attr_w");
        bool minOrMax = (in[0] == 0.0);
        Tango::DevDouble limit = in[1];

        if(minOrMax)
        {
            w_attr.set_min_value(limit);
        }
        else
        {
            w_attr.set_max_value(limit);
        }
    }

    static void attribute_factory(std::vector<Tango::Attr *> &attrs)
    {
        attrs.push_back(new TangoTest::AutoAttr<&AttrConfEventData::read_attr>("double_attr", Tango::DEV_DOUBLE));
        attrs.push_back(new TangoTest::AutoAttr<&AttrConfEventData::read_attr, &AttrConfEventData::write_attr>(
            "double_attr_w", Tango::DEV_DOUBLE));
    }

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

  private:
    Tango::DevDouble attr_dq_double{k_initial_value};
};

TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(AttrConfEventData, 1)

SCENARIO("Setting AttributeConfig works without database")
{
    int idlver = GENERATE(TangoTest::idlversion(1));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"double_attr", "AttrConfEventData", idlver};
        auto device = ctx.get_proxy();
        const std::string reset_value = "Not specified";

        REQUIRE(idlver == device->get_idl_version());

        THEN("we can change the attribute configuration")
        {
            using namespace TangoTest::Matchers;
            std::string attr{"double_attr"};
            auto ai = device->attribute_query(attr);
            ai.events.ch_event.abs_change = "33333";
            ai.events.ch_event.rel_change = "99.99";

            Tango::AttributeInfoListEx ail;
            ail.push_back(ai);
            REQUIRE_NOTHROW(device->set_attribute_config(ail));
        }
    }
}

SCENARIO("AttributeConfig returns correct data")
{
    int idlver = GENERATE(TangoTest::idlversion(4));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"double_attr", "AttrConfEventData", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();
        const std::string reset_value = "Not specified";

        REQUIRE(idlver == device->get_idl_version());

        AND_GIVEN("an attribute")
        {
            std::string attr{"double_attr"};

            THEN("we change the attribute configuration")
            {
                {
                    auto ai = device->attribute_query(attr);
                    ai.events.ch_event.abs_change = "33333";
                    ai.events.ch_event.rel_change = "99.99";

                    Tango::AttributeInfoListEx ail;
                    ail.push_back(ai);
                    device->set_attribute_config(ail);
                }

                WHEN("we subscribe to configuration change events for the attribute")
                {
                    TangoTest::CallbackMock<Tango::AttrConfEventData> callback;
                    const std::vector<std::string> filters;
                    TangoTest::Subscription sub{device, attr, Tango::ATTR_CONF_EVENT, &callback, filters};

                    require_initial_events(callback);

                    AND_THEN("change the configuration again")
                    {
                        auto ai = device->attribute_query(attr);
                        ai.events.ch_event.abs_change = reset_value;
                        ai.events.ch_event.rel_change = reset_value;

                        Tango::AttributeInfoListEx ail;
                        ail.push_back(ai);
                        device->set_attribute_config(ail);

                        THEN("we receive an event with the reset rel/abs change")
                        {
                            using namespace TangoTest::Matchers;

                            auto maybe_new_event = callback.pop_next_event();

                            REQUIRE(maybe_new_event != std::nullopt);
                            REQUIRE(maybe_new_event->attr_conf != nullptr);
                            REQUIRE_THAT(maybe_new_event, EventType(Tango::ATTR_CONF_EVENT));
                            REQUIRE(maybe_new_event->attr_conf->events.ch_event.abs_change == reset_value);
                            REQUIRE(maybe_new_event->attr_conf->events.ch_event.rel_change == reset_value);

                            AND_THEN("these are equal to the queried ones")
                            {
                                auto ai = device->attribute_query(attr);
                                REQUIRE(ai.events.ch_event.abs_change == reset_value);
                                REQUIRE(ai.events.ch_event.rel_change == reset_value);
                            }
                        }
                    }
                }
            }
        }
    }
}

SCENARIO("AttributeConfigurationEvent works")
{
    int idlver = GENERATE(TangoTest::idlversion(4));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"da", "AttrConfEventData", idlver};
        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();

        REQUIRE(idlver == device->get_idl_version());

        AND_GIVEN("an attribute")
        {
            std::string attr{"double_attr_w"};

            WHEN("we subscribe to configuration change events for the attribute")
            {
                TangoTest::CallbackMock<Tango::AttrConfEventData> cb1, cb2;
                const std::vector<std::string> filters;
                TangoTest::Subscription sub1{device, attr, Tango::ATTR_CONF_EVENT, &cb1, filters};
                TangoTest::Subscription sub2{device, attr, Tango::ATTR_CONF_EVENT, &cb2, filters};

                REQUIRE(!device->is_attribute_polled(attr));

                require_event(cb1);
                require_event(cb2);

                AND_THEN("change the configuration via a DS command (min and max)")
                {
                    Tango::DevVarDoubleArray dvda;
                    dvda.length(2);

                    // min to 0.0
                    dvda[0] = 0.0;
                    dvda[1] = 0.0;
                    Tango::DeviceData d_in;
                    d_in << dvda;
                    device->command_inout("set_attribute_limit", d_in);

                    // max to 10.0
                    dvda[0] = 1.0;
                    dvda[1] = 10.0;
                    d_in << dvda;
                    device->command_inout("set_attribute_limit", d_in);

                    THEN("we receive events with these new values")
                    {
                        using namespace TangoTest::Matchers;

                        auto check_event = [](auto event, std::string min_value, std::string max_value)
                        {
                            REQUIRE(event != std::nullopt);
                            REQUIRE(event->attr_conf != nullptr);
                            REQUIRE_THAT(event, EventType(Tango::ATTR_CONF_EVENT));
                            REQUIRE(!event->err);
                            REQUIRE(event->attr_conf->min_value == min_value);
                            REQUIRE(event->attr_conf->max_value == max_value);
                        };

                        auto event1 = cb1.pop_next_event();
                        check_event(event1, "0", "Not specified");

                        event1 = cb1.pop_next_event();
                        check_event(event1, "0", "10");

                        auto event2 = cb2.pop_next_event();
                        check_event(event2, "0", "Not specified");

                        event2 = cb2.pop_next_event();
                        check_event(event2, "0", "10");
                    }
                }
            }
        }
    }
}
