#include "catch2_common.h"
#include <regex>

#include <tango/internal/utils.h>

namespace
{
const std::string TestExceptReason = "Ahhh!";
const Tango::DevShort k_inital_short = 5678;
using CallbackMockType = TangoTest::CallbackMock<Tango::EventData>;

} // anonymous namespace

template <class Base>
class SimpleEventDevice : 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
        {
            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
        {
            TANGO_THROW_EXCEPTION(TestExceptReason, "This is a test");
        }
    }

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

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

  private:
    Tango::DevShort short_value{k_inital_short};
};

/* Returns true if all occurrences of
 * Attribute::set_client_lib(N,change)
 * in 'input' use the same N
 */
bool checkSameClientLib(const std::string &input)
{
    std::regex re(R"(Attribute::set_client_lib\(([0-9]+),change\))");
    std::smatch match;
    std::string::const_iterator it = input.cbegin();

    bool seenFirst = false;
    int firstValue = Tango::INVALID_IDL_VERSION;

    while(std::regex_search(it, input.cend(), match, re))
    {
        int val = parse_as<int>(match[1].str());

        if(!seenFirst)
        {
            firstValue = val;
            seenFirst = true;
        }
        else if(val != firstValue)
        {
            return false;
        }

        it = match.suffix().first;
    }

    if(!seenFirst)
    {
        return false;
    }

    return true;
}

TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(SimpleEventDevice, 4)

SCENARIO("Event re-subscribes with the same IDL", "[slow]")
{
    const std::string log_path = TangoTest::get_current_log_file_path();

    // the previous full contents across GENERATE
    static std::string prev_contents;

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

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

        AND_GIVEN("an change event subscription to that attribute")
        {
            TangoTest::CallbackMock<Tango::EventData> callback;
            TangoTest::Subscription sub{device, "Short_attr", Tango::CHANGE_EVENT, &callback};

            require_initial_events(callback);

            WHEN("when we restart server")
            {
                ctx.stop_server();
                ctx.restart_server();

                WHEN("an change event is generated after another error event")
                {
                    using namespace Catch::Matchers;
                    using namespace TangoTest::Matchers;

                    auto maybe_event = callback.pop_next_event(std::chrono::seconds{20});

                    REQUIRE(maybe_event != std::nullopt);
                    REQUIRE_THAT(maybe_event, EventErrorMatches(AllMatch(Reason(Tango::API_EventTimeout))));

                    maybe_event = callback.pop_next_event(std::chrono::seconds{20});

                    REQUIRE(maybe_event != std::nullopt);
                    REQUIRE_THAT(maybe_event, EventType(Tango::CHANGE_EVENT));

                    THEN("an change event is generated after another error event")
                    {
                        std::string all_server_log = load_file(log_path);

                        std::string new_chunk;
                        if(all_server_log.size() > prev_contents.size())
                        {
                            new_chunk = all_server_log.substr(prev_contents.size());
                        }
                        else
                        {
                            // log was truncated or rotated, then treat entire file as new
                            new_chunk = all_server_log;
                        }
                        prev_contents = std::move(all_server_log);

                        // sanity-check: we should actually have something new
                        REQUIRE(!new_chunk.empty());

                        REQUIRE(checkSameClientLib(new_chunk));

                        std::regex re(R"(zmq_client_idl_to_event_data_version\(([0-9]+),change\))");
                        std::smatch m;
                        REQUIRE(std::regex_search(new_chunk, m, re));
                        int found = parse_as<int>(m[1].str());
                        CHECK(found == idlver);
                    }
                }
            }
        }
    }
}
