#include "filedb/filedb_device.h"

#include <tango/tango.h>

#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_all.hpp>
#include <catch2/catch_translate_exception.hpp>
#include <catch2/generators/catch_generators.hpp>

#include <tuple>
#include <functional>

namespace Tango
{

inline const char *const *begin(const DevVarStringArray &cont)
{
    return cont.get_buffer();
}

inline const char *const *begin(const DevVarStringArray_var &var)
{
    auto cont = var.operator->();
    if(cont == nullptr)
    {
        return nullptr;
    }

    return begin(*cont);
}

inline const char *const *end(const DevVarStringArray &cont)
{
    return cont.get_buffer() + cont.length();
}

inline const char *const *end(const DevVarStringArray_var &var)
{
    auto cont = var.operator->();
    if(cont == nullptr)
    {
        return nullptr;
    }

    return end(*cont);
}

} // namespace Tango

SCENARIO("We can get database info")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        WHEN("We call info()")
        {
            Tango::DevVarStringArray_var info = backend.info();

            THEN("We find information about an experimental filedb server")
            {
                using Catch::Matchers::StartsWith;
                REQUIRE(info->length() == 13);
                REQUIRE_THAT(info[0].in(), StartsWith("Experimental FileDbDs"));
            }
        }
    }
}

SCENARIO("No properties")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        using Data = std::tuple<const char *, decltype(&FileDb::FileDbDeviceBackend::get_property), size_t>;

        auto data = GENERATE(Data{"free object", &FileDb::FileDbDeviceBackend::get_property, 8},
                             Data{"device", &FileDb::FileDbDeviceBackend::get_device_property, 8},
                             Data{"class", &FileDb::FileDbDeviceBackend::get_class_property, 8},
                             Data{"device attribute", &FileDb::FileDbDeviceBackend::get_device_attribute_property2, 6},
                             Data{"class attribute", &FileDb::FileDbDeviceBackend::get_class_attribute_property2, 6},
                             Data{"device pipe", &FileDb::FileDbDeviceBackend::get_device_pipe_property, 2},
                             Data{"class pipe", &FileDb::FileDbDeviceBackend::get_class_pipe_property, 2});

        WHEN("We query for some " << std::get<0>(data) << " properties")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(3);
            in[0] = Tango::string_dup("non_existent");
            in[1] = Tango::string_dup("prop_or_attr_or_pipe1");
            in[2] = Tango::string_dup("prop_or_attr_or_pipe2");
            Tango::DevVarStringArray_var properties = std::invoke(std::get<1>(data), backend, in.in());

            THEN("We get no data back")
            {
                size_t expected_length = std::get<2>(data);

                switch(expected_length)
                {
                case 8:
                    // Object name, property count, then for example prop:
                    // property name, value count, empty value
                    REQUIRE(properties->length() == 8);
                    REQUIRE(std::string_view{properties[0].in()} == "non_existent");
                    REQUIRE(std::string_view{properties[1].in()} == "2");
                    REQUIRE(std::string_view{properties[2].in()} == "prop_or_attr_or_pipe1");
                    REQUIRE(std::string_view{properties[3].in()} == "0");
                    REQUIRE(std::string_view{properties[4].in()} == " ");
                    REQUIRE(std::string_view{properties[5].in()} == "prop_or_attr_or_pipe2");
                    REQUIRE(std::string_view{properties[6].in()} == "0");
                    REQUIRE(std::string_view{properties[7].in()} == " ");
                    break;
                case 6:
                    // Object name, attribute count, then for example attribute:
                    //  attribute name, property count
                    REQUIRE(properties->length() == 6);
                    REQUIRE(std::string_view{properties[0].in()} == "non_existent");
                    REQUIRE(std::string_view{properties[1].in()} == "2");
                    REQUIRE(std::string_view{properties[2].in()} == "prop_or_attr_or_pipe1");
                    REQUIRE(std::string_view{properties[3].in()} == "0");
                    REQUIRE(std::string_view{properties[4].in()} == "prop_or_attr_or_pipe2");
                    REQUIRE(std::string_view{properties[5].in()} == "0");
                    break;
                case 2:
                    // Object name, pipe count
                    REQUIRE(properties->length() == 2);
                    REQUIRE(std::string_view{properties[0].in()} == "non_existent");
                    REQUIRE(std::string_view{properties[1].in()} == "0");
                    break;
                }
            }
        }
    }
}

SCENARIO("Read free object/class/device properties")
{
    GIVEN("An FileDB backend with some property data")
    {
        FileDb::DbConfigTables tables;

        tables.put_record(FileDb::FreeObjectPropertyRecord{"obj/prop_scalar", {"value"}});
        tables.put_record(FileDb::FreeObjectPropertyRecord{"obj/prop_spectrum", {"value1", "value2"}});

        tables.put_record(FileDb::ClassPropertyRecord{"class/prop_scalar", {"value"}});
        tables.put_record(FileDb::ClassPropertyRecord{"class/prop_spectrum", {"value1", "value2"}});

        tables.put_record(FileDb::DevicePropertyRecord{"foo/bar/baz/prop_scalar", {"value"}});
        tables.put_record(FileDb::DevicePropertyRecord{"foo/bar/baz/prop_spectrum", {"value1", "value2"}});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        using Data = std::tuple<const char *, const char *, decltype(&FileDb::FileDbDeviceBackend::get_property)>;

        auto data = GENERATE(Data{"free object", "obj", &FileDb::FileDbDeviceBackend::get_property},
                             Data{"device", "foo/bar/baz", &FileDb::FileDbDeviceBackend::get_device_property},
                             Data{"class", "class", &FileDb::FileDbDeviceBackend::get_class_property});

        WHEN("We query for some " << std::get<0>(data) << " properties")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(4);
            in[0] = Tango::string_dup(std::get<1>(data));
            in[1] = Tango::string_dup("prop_scalar");
            in[2] = Tango::string_dup("prop_spectrum");
            in[3] = Tango::string_dup("non_existant");
            Tango::DevVarStringArray_var properties = std::invoke(std::get<2>(data), backend, in.in());

            THEN("We get the property data")
            {
                REQUIRE(properties->length() == 12);
                REQUIRE(std::string_view{properties[0].in()} == std::get<1>(data));
                REQUIRE(std::string_view{properties[1].in()} == "3");
                REQUIRE(std::string_view{properties[2].in()} == "prop_scalar");
                REQUIRE(std::string_view{properties[3].in()} == "1");
                REQUIRE(std::string_view{properties[4].in()} == "value");
                REQUIRE(std::string_view{properties[5].in()} == "prop_spectrum");
                REQUIRE(std::string_view{properties[6].in()} == "2");
                REQUIRE(std::string_view{properties[7].in()} == "value1");
                REQUIRE(std::string_view{properties[8].in()} == "value2");
                REQUIRE(std::string_view{properties[9].in()} == "non_existant");
                REQUIRE(std::string_view{properties[10].in()} == "0");
                REQUIRE(std::string_view{properties[11].in()} == " ");
            }
        }
    }
}

SCENARIO("Read attribute properties")
{
    GIVEN("An FileDB backend with some property data")
    {
        FileDb::DbConfigTables tables;

        tables.put_record(FileDb::ClassAttributePropertyRecord{"class/attr/prop_scalar", {"value"}});
        tables.put_record(FileDb::ClassAttributePropertyRecord{"class/attr/prop_spectrum", {"value1", "value2"}});

        tables.put_record(FileDb::DeviceAttributePropertyRecord{"foo/bar/baz/attr/prop_scalar", {"value"}});
        tables.put_record(
            FileDb::DeviceAttributePropertyRecord{"foo/bar/baz/attr/prop_spectrum", {"value1", "value2"}});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        using Data = std::tuple<const char *, const char *, decltype(&FileDb::FileDbDeviceBackend::get_property)>;

        auto data = GENERATE(
            Data{"device attribute", "foo/bar/baz", &FileDb::FileDbDeviceBackend::get_device_attribute_property2},
            Data{"class attribute", "class", &FileDb::FileDbDeviceBackend::get_class_attribute_property2});

        WHEN("We query for some " << std::get<0>(data) << " properties")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(3);
            in[0] = Tango::string_dup(std::get<1>(data));
            in[1] = Tango::string_dup("attr");
            in[2] = Tango::string_dup("non_existent");
            Tango::DevVarStringArray_var properties = std::invoke(std::get<2>(data), backend, in.in());

            THEN("We get the property data")
            {
                REQUIRE(properties->length() == 13);
                REQUIRE(std::string_view{properties[0].in()} == std::get<1>(data));
                REQUIRE(std::string_view{properties[1].in()} == "2");
                REQUIRE(std::string_view{properties[2].in()} == "attr");
                REQUIRE(std::string_view{properties[3].in()} == "2");
                REQUIRE(std::string_view{properties[4].in()} == "prop_scalar");
                REQUIRE(std::string_view{properties[5].in()} == "1");
                REQUIRE(std::string_view{properties[6].in()} == "value");
                REQUIRE(std::string_view{properties[7].in()} == "prop_spectrum");
                REQUIRE(std::string_view{properties[8].in()} == "2");
                REQUIRE(std::string_view{properties[9].in()} == "value1");
                REQUIRE(std::string_view{properties[10].in()} == "value2");
                REQUIRE(std::string_view{properties[11].in()} == "non_existent");
                REQUIRE(std::string_view{properties[12].in()} == "0");
            }
        }
    }
}

SCENARIO("Device property list")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        WHEN("We query for a device property list")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(2);
            in[0] = Tango::string_dup("foo/bar/baz");
            in[1] = Tango::string_dup("*");
            Tango::DevVarStringArray_var properties = backend.get_device_property_list(in.in());

            THEN("We find no properties")
            {
                REQUIRE(properties->length() == 0);
            }
        }
    }

    GIVEN("An FileDB backend with some device property data")
    {
        FileDb::DbConfigTables tables;

        tables.put_record(FileDb::DevicePropertyRecord{"foo/bar/baz/prop_scalar", {"value"}});
        tables.put_record(FileDb::DevicePropertyRecord{"foo/bar/baz/prop_spectrum", {"value1", "value2"}});
        tables.put_record(FileDb::DevicePropertyRecord{"foo/bar/baz/prop2_spectrum", {"value1", "value2"}});

        tables.put_record(FileDb::DevicePropertyRecord{"foo/bar/qux/prop3_scalar", {"value"}});
        tables.put_record(FileDb::DevicePropertyRecord{"foo/bar/qux/prop3_spectrum", {"value1", "value2"}});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        WHEN("We query for a device property list")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(2);
            in[0] = Tango::string_dup("foo/bar/baz");
            in[1] = Tango::string_dup("*");
            Tango::DevVarStringArray_var properties = backend.get_device_property_list(in.in());

            THEN("We find all properties for that device")
            {
                using Catch::Matchers::Contains;

                REQUIRE(properties->length() == 3);
                REQUIRE_THAT(properties, Contains(std::string_view{"prop_scalar"}));
                REQUIRE_THAT(properties, Contains(std::string_view{"prop_spectrum"}));
                REQUIRE_THAT(properties, Contains(std::string_view{"prop2_spectrum"}));
            }
        }

        WHEN("We query for a device property list without a wildcard")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(2);
            in[0] = Tango::string_dup("foo/bar/baz");
            in[1] = Tango::string_dup("prop2_spectrum");
            Tango::DevVarStringArray_var properties = backend.get_device_property_list(in.in());

            THEN("We find get just that property back")
            {
                REQUIRE(properties->length() == 1);
                REQUIRE(std::string_view{properties[0].in()} == "prop2_spectrum");
            }
        }
    }
}

SCENARIO("Device list by server/class")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        WHEN("We query for all devices")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(2);
            in[0] = Tango::string_dup("*");
            in[1] = Tango::string_dup("*");
            Tango::DevVarStringArray_var devices = backend.get_device_list(in.in());

            THEN("We find no devices")
            {
                REQUIRE(devices->length() == 0);
            }
        }
    }

    GIVEN("An FileDB backend with some servers configured")
    {
        FileDb::DbConfigTables tables;

        tables.put_record(FileDb::ServerRecord{"exe/inst1/class1", {"foo/bar/baz", "foo/bar/qux"}});
        tables.put_record(FileDb::ServerRecord{"exe/inst1/class2", {"foo/bar/quux"}});
        tables.put_record(FileDb::ServerRecord{"exe/inst2/class1", {"foo/bar/quuux"}});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        WHEN("We query for all devices")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(2);
            in[0] = Tango::string_dup("*");
            in[1] = Tango::string_dup("*");
            Tango::DevVarStringArray_var devices = backend.get_device_list(in.in());

            THEN("We find all the devices")
            {
                using Catch::Matchers::Contains;

                REQUIRE(devices->length() == 4);
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/baz"}));
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/qux"}));
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/quux"}));
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/quuux"}));
            }
        }

        WHEN("We query for devices for a specific server/class pair")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(2);
            in[0] = Tango::string_dup("exe/inst1");
            in[1] = Tango::string_dup("class1");
            Tango::DevVarStringArray_var devices = backend.get_device_list(in.in());

            THEN("We find just those devices")
            {
                using Catch::Matchers::Contains;

                REQUIRE(devices->length() == 2);
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/baz"}));
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/qux"}));
            }
        }

        WHEN("We query for devices for a specific server")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(2);
            in[0] = Tango::string_dup("exe/inst1");
            in[1] = Tango::string_dup("*");
            Tango::DevVarStringArray_var devices = backend.get_device_list(in.in());

            THEN("We find just those devices")
            {
                using Catch::Matchers::Contains;

                REQUIRE(devices->length() == 3);
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/baz"}));
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/qux"}));
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/quux"}));
            }
        }

        WHEN("We query for devices for a specific class")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(2);
            in[0] = Tango::string_dup("*");
            in[1] = Tango::string_dup("class1");
            Tango::DevVarStringArray_var devices = backend.get_device_list(in.in());

            THEN("We find just those devices")
            {
                using Catch::Matchers::Contains;

                REQUIRE(devices->length() == 3);
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/baz"}));
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/qux"}));
                REQUIRE_THAT(devices, Contains(std::string_view{"foo/bar/quuux"}));
            }
        }
    }
}

SCENARIO("Query device name parts")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        using Data = std::tuple<const char *, decltype(&FileDb::FileDbDeviceBackend::get_device_domain_list)>;
        auto data = GENERATE(Data{"domain", &FileDb::FileDbDeviceBackend::get_device_domain_list},
                             Data{"family", &FileDb::FileDbDeviceBackend::get_device_family_list},
                             Data{"member", &FileDb::FileDbDeviceBackend::get_device_member_list});

        WHEN("We query for all " << std::get<0>(data))
        {
            Tango::DevString in = Tango::string_dup("*");
            Tango::DevVarStringArray_var name_parts = std::invoke(std::get<1>(data), backend, in);
            Tango::string_free(in);

            THEN("We find no " << std::get<0>(data) << "s")
            {
                REQUIRE(name_parts->length() == 0);
            }
        }
    }

    GIVEN("A FileDB backend with some devices defined")
    {
        FileDb::DbRuntimeTables tables;

        tables.put_record(FileDb::DeviceRecord{"foo/bar/baz", "exe/inst", "device_class"});
        tables.put_record(FileDb::DeviceRecord{"foo/bar/qux", "exe/inst", "device_class"});
        tables.put_record(FileDb::DeviceRecord{"bar/baz/foo", "exe/inst", "device_class"});
        tables.put_record(FileDb::DeviceRecord{"bar/baz/qux", "exe/inst", "device_class"});
        tables.put_record(FileDb::DeviceRecord{"baz/foo/bar", "exe/inst", "device_class"});
        tables.put_record(FileDb::DeviceRecord{"baz/foo/qux", "exe/inst", "device_class"});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        using Data = std::tuple<const char *,
                                decltype(&FileDb::FileDbDeviceBackend::get_device_domain_list),
                                std::vector<const char *>,
                                const char *>;
        auto data = GENERATE(
            Data{"domain", &FileDb::FileDbDeviceBackend::get_device_domain_list, {"foo", "bar", "baz"}, "b*/*/*"},
            Data{"family", &FileDb::FileDbDeviceBackend::get_device_family_list, {"foo", "bar", "baz"}, "*/b*/*"},
            Data{"member",
                 &FileDb::FileDbDeviceBackend::get_device_member_list,
                 {"foo", "bar", "baz", "qux"},
                 "*/*/b*"});

        WHEN("We query for all " << std::get<0>(data))
        {
            Tango::DevString in = Tango::string_dup("*");
            Tango::DevVarStringArray_var name_parts = std::invoke(std::get<1>(data), backend, in);
            Tango::string_free(in);

            THEN("We find all the " << std::get<0>(data) << "s")
            {
                using Catch::Matchers::Contains;

                auto &expected = std::get<2>(data);
                REQUIRE(name_parts->length() == expected.size());
                for(const char *e : expected)
                {
                    REQUIRE_THAT(name_parts, Contains(std::string_view{e}));
                }
            }
        }

        WHEN("We query for " << std::get<0>(data) << "s starting with b")
        {
            Tango::DevString in = Tango::string_dup(std::get<3>(data));
            Tango::DevVarStringArray_var name_parts = std::invoke(std::get<1>(data), backend, in);
            Tango::string_free(in);

            THEN("We find \"bar\" and \"baz\"")
            {
                using Catch::Matchers::Contains;
                REQUIRE(name_parts->length() == 2);
                REQUIRE_THAT(name_parts, Contains(std::string_view{"bar"}));
                REQUIRE_THAT(name_parts, Contains(std::string_view{"baz"}));
            }
        }
    }
}

SCENARIO("Query device names")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        using Data = std::tuple<const char *, decltype(&FileDb::FileDbDeviceBackend::get_device_wide_list)>;
        auto data = GENERATE(Data{"all", &FileDb::FileDbDeviceBackend::get_device_wide_list},
                             Data{"exported", &FileDb::FileDbDeviceBackend::get_device_exported_list});

        WHEN("We query for " << std::get<0>(data) << " devices")
        {
            Tango::DevString in = Tango::string_dup("*");
            Tango::DevVarStringArray_var name_parts = std::invoke(std::get<1>(data), backend, in);
            Tango::string_free(in);

            THEN("We find no devices")
            {
                REQUIRE(name_parts->length() == 0);
            }
        }
    }

    GIVEN("A FileDB backend with some devices defined")
    {
        FileDb::DbRuntimeTables tables;

        tables.put_record(FileDb::DeviceRecord{"foo/bar/baz", "exe/inst", "device_class"});
        tables.put_record(FileDb::DeviceRecord{"foo/bar/qux", "exe/inst", "device_class", true});
        tables.put_record(FileDb::DeviceRecord{"bar/baz/foo", "exe/inst", "device_class"});
        tables.put_record(FileDb::DeviceRecord{"bar/baz/qux", "exe/inst", "device_class", true});
        tables.put_record(FileDb::DeviceRecord{"baz/foo/bar", "exe/inst", "device_class"});
        tables.put_record(FileDb::DeviceRecord{"baz/foo/qux", "exe/inst", "device_class", true});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        using Data = std::tuple<const char *,
                                decltype(&FileDb::FileDbDeviceBackend::get_device_wide_list),
                                std::vector<const char *>>;
        auto data = GENERATE(Data{"all",
                                  &FileDb::FileDbDeviceBackend::get_device_wide_list,
                                  {
                                      "foo/bar/baz",
                                      "foo/bar/qux",
                                      "bar/baz/foo",
                                      "bar/baz/qux",
                                      "baz/foo/bar",
                                      "baz/foo/qux",
                                  }},
                             Data{"exported",
                                  &FileDb::FileDbDeviceBackend::get_device_exported_list,
                                  {
                                      "foo/bar/qux",
                                      "bar/baz/qux",
                                      "baz/foo/qux",
                                  }});

        WHEN("We query for " << std::get<0>(data) << " devices")
        {
            Tango::DevString in = Tango::string_dup("*");
            Tango::DevVarStringArray_var name_parts = std::invoke(std::get<1>(data), backend, in);
            Tango::string_free(in);

            THEN("We find " << std::get<0>(data) << " devices")
            {
                using Catch::Matchers::Contains;

                auto &expected = std::get<2>(data);
                REQUIRE(name_parts->length() == expected.size());
                for(const char *e : expected)
                {
                    REQUIRE_THAT(name_parts, Contains(std::string_view{e}));
                }
            }
        }

        WHEN("We query for " << std::get<0>(data) << " devices with domain \"baz\"")
        {
            Tango::DevString in = Tango::string_dup("baz/*/*");
            Tango::DevVarStringArray_var name_parts = std::invoke(std::get<1>(data), backend, in);
            Tango::string_free(in);

            THEN("We find " << std::get<0>(data) << " devices")
            {
                using Catch::Matchers::Contains;

                auto &expected = std::get<2>(data);
                size_t expected_size = 0;
                for(const char *e : expected)
                {
                    if(std::string_view{e}.find("baz") == 0)
                    {
                        expected_size += 1;
                        REQUIRE_THAT(name_parts, Contains(std::string_view{e}));
                    }
                }
                REQUIRE(name_parts->length() == expected_size);
            }
        }
    }
}

SCENARIO("Query servers")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        using Data = std::tuple<const char *, decltype(&FileDb::FileDbDeviceBackend::get_server_list)>;
        auto data = GENERATE(Data{"instances", &FileDb::FileDbDeviceBackend::get_server_list},
                             Data{"executables", &FileDb::FileDbDeviceBackend::get_server_name_list});

        WHEN("We query for all server " << std::get<0>(data))
        {
            Tango::DevString in = Tango::string_dup("*");
            Tango::DevVarStringArray_var names = std::invoke(std::get<1>(data), backend, in);
            Tango::string_free(in);

            THEN("We find no names")
            {
                REQUIRE(names->length() == 0);
            }
        }
    }

    GIVEN("A FileDB backend with some server defines")
    {
        FileDb::DbConfigTables tables;

        tables.put_record(FileDb::ServerRecord{"Exe1/inst1"});
        tables.put_record(FileDb::ServerRecord{"Exe1/inst2"});
        tables.put_record(FileDb::ServerRecord{"Exe2/inst1"});
        tables.put_record(FileDb::ServerRecord{"Exe2/inst2"});
        tables.put_record(FileDb::ServerRecord{"Exe3/inst1"});
        tables.put_record(FileDb::ServerRecord{"Exe3/inst2"});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        using Data = std::tuple<const char *,
                                decltype(&FileDb::FileDbDeviceBackend::get_server_list),
                                std::vector<const char *>,
                                std::vector<const char *>>;
        auto data = GENERATE(Data{"instances",
                                  &FileDb::FileDbDeviceBackend::get_server_list,
                                  {
                                      "Exe1/inst1",
                                      "Exe1/inst2",
                                      "Exe2/inst1",
                                      "Exe2/inst2",
                                      "Exe3/inst1",
                                      "Exe3/inst2",
                                  },
                                  {
                                      "Exe1/inst1",
                                      "Exe2/inst1",
                                      "Exe3/inst1",
                                  }},
                             Data{"executables",
                                  &FileDb::FileDbDeviceBackend::get_server_name_list,
                                  {"Exe1", "Exe2", "Exe3"},
                                  {"Exe1", "Exe2", "Exe3"}});

        WHEN("We query for all server " << std::get<0>(data))
        {
            Tango::DevString in = Tango::string_dup("*");
            Tango::DevVarStringArray_var names = std::invoke(std::get<1>(data), backend, in);
            Tango::string_free(in);

            THEN("We find all server " << std::get<0>(data))
            {
                using Catch::Matchers::Contains;

                auto &expected = std::get<2>(data);
                REQUIRE(names->length() == expected.size());
                for(const char *e : expected)
                {
                    REQUIRE_THAT(names, Contains(std::string_view{e}));
                }
            }
        }

        WHEN("We query for server " << std::get<0>(data) << " with a specific instance name")
        {
            Tango::DevString in = Tango::string_dup("*/inst1");
            Tango::DevVarStringArray_var names = std::invoke(std::get<1>(data), backend, in);
            Tango::string_free(in);

            THEN("We find all server " << std::get<0>(data))
            {
                using Catch::Matchers::Contains;

                auto &expected = std::get<3>(data);
                REQUIRE(names->length() == expected.size());
                for(const char *e : expected)
                {
                    REQUIRE_THAT(names, Contains(std::string_view{e}));
                }
            }
        }
    }
}

SCENARIO("Adding a server")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        WHEN("We add a server")
        {
            using Catch::Matchers::Contains;
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(7);
            in[0] = Tango::string_dup("Exe/inst");
            in[1] = Tango::string_dup("foo/bar/baz");
            in[2] = Tango::string_dup("device_class1");
            in[3] = Tango::string_dup("foo/bar/quux");
            in[4] = Tango::string_dup("device_class2");
            in[5] = Tango::string_dup("foo/bar/qux");
            in[6] = Tango::string_dup("device_class1");
            backend.add_server(in.in());

            THEN("We have ServerRecords for each server/class pair")
            {
                using Catch::Matchers::SizeIs;

                const auto &tables = backend.config_tables();
                const auto &table = tables.get_table<FileDb::ServerRecord>();

                REQUIRE_THAT(table, SizeIs(2));

                {
                    auto found = std::find_if(
                        table.begin(), table.end(), [](const auto &r) { return r.ident == "Exe/inst/device_class1"; });

                    REQUIRE(found != table.end());
                    REQUIRE_THAT(found->devices, Contains("foo/bar/baz"));
                    REQUIRE_THAT(found->devices, Contains("foo/bar/qux"));
                }

                {
                    auto found = std::find_if(
                        table.begin(), table.end(), [](const auto &r) { return r.ident == "Exe/inst/device_class2"; });

                    REQUIRE(found != table.end());
                    REQUIRE_THAT(found->devices, Contains("foo/bar/quux"));
                }

                AND_THEN("We have DeviceRecords for each device")
                {
                    const auto &tables = backend.runtime_tables();
                    const auto &table = tables.get_table<FileDb::DeviceRecord>();

                    REQUIRE_THAT(table, SizeIs(3));

                    {
                        auto found = std::find_if(
                            table.begin(), table.end(), [](const auto &r) { return r.ident == "foo/bar/baz"; });

                        REQUIRE(found != table.end());
                        REQUIRE(found->server == "Exe/inst");
                        REQUIRE(found->device_class == "device_class1");
                        REQUIRE(!found->exported);
                    }

                    {
                        auto found = std::find_if(
                            table.begin(), table.end(), [](const auto &r) { return r.ident == "foo/bar/qux"; });

                        REQUIRE(found != table.end());
                        REQUIRE(found->server == "Exe/inst");
                        REQUIRE(found->device_class == "device_class1");
                        REQUIRE(!found->exported);
                    }

                    {
                        auto found = std::find_if(
                            table.begin(), table.end(), [](const auto &r) { return r.ident == "foo/bar/quux"; });

                        REQUIRE(found != table.end());
                        REQUIRE(found->server == "Exe/inst");
                        REQUIRE(found->device_class == "device_class2");
                        REQUIRE(!found->exported);
                    }
                }
            }
        }
    }
}

SCENARIO("Putting free object properties")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        WHEN("We put some free object properties")
        {
            using Catch::Matchers::RangeEquals;

            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(9);
            in[0] = Tango::string_dup("obj");
            in[1] = Tango::string_dup("2");
            in[2] = Tango::string_dup("prop1");
            in[3] = Tango::string_dup("1");
            in[4] = Tango::string_dup("value1");
            in[5] = Tango::string_dup("prop2");
            in[6] = Tango::string_dup("2");
            in[7] = Tango::string_dup("value1");
            in[8] = Tango::string_dup("value2");
            backend.put_property(in.in());

            THEN("We have FreeObjectPropertyRecords for each property")
            {
                using Catch::Matchers::SizeIs;

                const auto &tables = backend.config_tables();
                const auto &table = tables.get_table<FileDb::FreeObjectPropertyRecord>();

                REQUIRE_THAT(table, SizeIs(2));

                {
                    auto found =
                        std::find_if(table.begin(), table.end(), [](const auto &r) { return r.ident == "obj/prop1"; });

                    REQUIRE(found != table.end());
                    REQUIRE_THAT(found->values, RangeEquals<std::vector<const char *>>({"value1"}));
                }

                {
                    auto found =
                        std::find_if(table.begin(), table.end(), [](const auto &r) { return r.ident == "obj/prop2"; });

                    REQUIRE(found != table.end());
                    REQUIRE_THAT(found->values, RangeEquals<std::vector<const char *>>({"value1", "value2"}));
                }
            }
        }
    }
}

SCENARIO("Deleting free object properties")
{
    GIVEN("An FileDB backend with some device property data")
    {
        FileDb::DbConfigTables tables;

        tables.put_record(FileDb::FreeObjectPropertyRecord{"obj/prop1", {"value"}});
        tables.put_record(FileDb::FreeObjectPropertyRecord{"obj/prop2", {"value"}});
        tables.put_record(FileDb::FreeObjectPropertyRecord{"obj/prop3", {"value"}});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        WHEN("we delete some class properties")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(3);
            in[0] = Tango::string_dup("obj");
            in[1] = Tango::string_dup("prop1");
            in[2] = Tango::string_dup("prop2");
            backend.delete_property(in.in());

            THEN("we only have ClassPropertyRecords for the remaining property")
            {
                using Catch::Matchers::SizeIs;

                const auto &tables = backend.config_tables();
                const auto &table = tables.get_table<FileDb::FreeObjectPropertyRecord>();

                REQUIRE_THAT(table, SizeIs(1));
                REQUIRE(table[0].ident == "obj/prop3");
            }
        }
    }
}

SCENARIO("Putting device properties")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        WHEN("We put some device properties")
        {
            using Catch::Matchers::RangeEquals;

            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(9);
            in[0] = Tango::string_dup("foo/bar/baz");
            in[1] = Tango::string_dup("2");
            in[2] = Tango::string_dup("prop1");
            in[3] = Tango::string_dup("1");
            in[4] = Tango::string_dup("value1");
            in[5] = Tango::string_dup("prop2");
            in[6] = Tango::string_dup("2");
            in[7] = Tango::string_dup("value1");
            in[8] = Tango::string_dup("value2");
            backend.put_device_property(in.in());

            THEN("We have DevicePropertyRecords for each property")
            {
                using Catch::Matchers::SizeIs;

                const auto &tables = backend.config_tables();
                const auto &table = tables.get_table<FileDb::DevicePropertyRecord>();

                REQUIRE_THAT(table, SizeIs(2));

                {
                    auto found = std::find_if(
                        table.begin(), table.end(), [](const auto &r) { return r.ident == "foo/bar/baz/prop1"; });

                    REQUIRE(found != table.end());
                    REQUIRE_THAT(found->values, RangeEquals<std::vector<const char *>>({"value1"}));
                }

                {
                    auto found = std::find_if(
                        table.begin(), table.end(), [](const auto &r) { return r.ident == "foo/bar/baz/prop2"; });

                    REQUIRE(found != table.end());
                    REQUIRE_THAT(found->values, RangeEquals<std::vector<const char *>>({"value1", "value2"}));
                }
            }
        }
    }
}

SCENARIO("Deleting device properties")
{
    GIVEN("An FileDB backend with some device property data")
    {
        FileDb::DbConfigTables tables;

        tables.put_record(FileDb::DevicePropertyRecord{"foo/bar/baz/prop1", {"value"}});
        tables.put_record(FileDb::DevicePropertyRecord{"foo/bar/baz/prop2", {"value"}});
        tables.put_record(FileDb::DevicePropertyRecord{"foo/bar/baz/prop3", {"value"}});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        WHEN("We delete some device properties")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(3);
            in[0] = Tango::string_dup("foo/bar/baz");
            in[1] = Tango::string_dup("prop1");
            in[2] = Tango::string_dup("prop2");
            backend.delete_device_property(in.in());

            THEN("We only have DevicePropertyRecords for the remaining property")
            {
                using Catch::Matchers::SizeIs;

                const auto &tables = backend.config_tables();
                const auto &table = tables.get_table<FileDb::DevicePropertyRecord>();

                REQUIRE_THAT(table, SizeIs(1));
                REQUIRE(table[0].ident == "foo/bar/baz/prop3");
            }
        }
    }
}

SCENARIO("Putting class properties")
{
    GIVEN("An empty FileDB backend")
    {
        FileDb::FileDbDeviceBackend backend;

        WHEN("We put some class properties")
        {
            using Catch::Matchers::RangeEquals;

            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(9);
            in[0] = Tango::string_dup("device_class");
            in[1] = Tango::string_dup("2");
            in[2] = Tango::string_dup("prop1");
            in[3] = Tango::string_dup("1");
            in[4] = Tango::string_dup("value1");
            in[5] = Tango::string_dup("prop2");
            in[6] = Tango::string_dup("2");
            in[7] = Tango::string_dup("value1");
            in[8] = Tango::string_dup("value2");
            backend.put_class_property(in.in());

            THEN("We have ClassPropertyRecords for each property")
            {
                using Catch::Matchers::SizeIs;

                const auto &tables = backend.config_tables();
                const auto &table = tables.get_table<FileDb::ClassPropertyRecord>();

                REQUIRE_THAT(table, SizeIs(2));

                {
                    auto found = std::find_if(
                        table.begin(), table.end(), [](const auto &r) { return r.ident == "device_class/prop1"; });

                    REQUIRE(found != table.end());
                    REQUIRE_THAT(found->values, RangeEquals<std::vector<const char *>>({"value1"}));
                }

                {
                    auto found = std::find_if(
                        table.begin(), table.end(), [](const auto &r) { return r.ident == "device_class/prop2"; });

                    REQUIRE(found != table.end());
                    REQUIRE_THAT(found->values, RangeEquals<std::vector<const char *>>({"value1", "value2"}));
                }
            }
        }
    }
}

SCENARIO("Deleting class properties")
{
    GIVEN("An FileDB backend with some device property data")
    {
        FileDb::DbConfigTables tables;

        tables.put_record(FileDb::ClassPropertyRecord{"device_class/prop1", {"value"}});
        tables.put_record(FileDb::ClassPropertyRecord{"device_class/prop2", {"value"}});
        tables.put_record(FileDb::ClassPropertyRecord{"device_class/prop3", {"value"}});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        WHEN("we delete some class properties")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(3);
            in[0] = Tango::string_dup("device_class");
            in[1] = Tango::string_dup("prop1");
            in[2] = Tango::string_dup("prop2");
            backend.delete_class_property(in.in());

            THEN("we only have ClassPropertyRecords for the remaining property")
            {
                using Catch::Matchers::SizeIs;

                const auto &tables = backend.config_tables();
                const auto &table = tables.get_table<FileDb::ClassPropertyRecord>();

                REQUIRE_THAT(table, SizeIs(1));
                REQUIRE(table[0].ident == "device_class/prop3");
            }
        }
    }
}

SCENARIO("Import and export devices")
{
    GIVEN("A FileDB backend with a device defined")
    {
        FileDb::DbRuntimeTables tables;

        tables.put_record(FileDb::DeviceRecord{"foo/bar/baz", "exe/inst", "device_class"});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        WHEN("We import the device")
        {
            Tango::DevString in = Tango::string_dup("foo/bar/baz");
            Tango::DevVarLongStringArray_var info = backend.import_device(in);
            Tango::string_free(in);

            THEN("We find the expected information for a device that has never been exported")
            {
                REQUIRE(info->svalue.length() == 8);
                REQUIRE(info->lvalue.length() == 2);

                REQUIRE(std::string_view{info->svalue[0]} == "foo/bar/baz");
                REQUIRE(std::string_view{info->svalue[1]} == "nada"); // IOR
                REQUIRE(std::string_view{info->svalue[2]} == "nada"); // Version
                REQUIRE(std::string_view{info->svalue[3]} == "exe/inst");
                REQUIRE(std::string_view{info->svalue[4]} == "nada"); // host
                REQUIRE(std::string_view{info->svalue[5]} == "?");    // start time
                REQUIRE(std::string_view{info->svalue[6]} == "?");    // stop time
                REQUIRE(std::string_view{info->svalue[7]} == "device_class");

                REQUIRE(info->lvalue[0] == 0);  // exported
                REQUIRE(info->lvalue[1] == -1); // pid
            }
        }

        WHEN("We export the device")
        {
            Tango::DevVarStringArray_var in = new Tango::DevVarStringArray;
            in->length(5);
            in[0] = Tango::string_dup("foo/bar/baz");
            in[1] = Tango::string_dup("MYIOR");
            in[2] = Tango::string_dup("hostname");
            in[3] = Tango::string_dup("12345");
            in[4] = Tango::string_dup("version");
            backend.export_device(in.in());

            THEN("We have a DeviceRecord which is exported")
            {
                using Catch::Matchers::SizeIs;

                const auto &tables = backend.runtime_tables();
                const auto &table = tables.get_table<FileDb::DeviceRecord>();

                REQUIRE_THAT(table, SizeIs(1));

                REQUIRE(table[0].ident == "foo/bar/baz");
                REQUIRE(table[0].exported);
                REQUIRE(table[0].server == "exe/inst");
                REQUIRE(table[0].device_class == "device_class");
                REQUIRE(table[0].ior == "MYIOR");
                REQUIRE(table[0].host == "hostname");
                REQUIRE(table[0].pid == 12345);
                REQUIRE(table[0].started != std::nullopt);
                REQUIRE(table[0].stopped == std::nullopt);
            }

            AND_WHEN("We import the device")
            {
                Tango::DevString in = Tango::string_dup("foo/bar/baz");
                Tango::DevVarLongStringArray_var info = backend.import_device(in);
                Tango::string_free(in);

                THEN("We find the expected information for an exported device")
                {
                    REQUIRE(info->svalue.length() == 8);
                    REQUIRE(info->lvalue.length() == 2);

                    REQUIRE(std::string_view{info->svalue[0]} == "foo/bar/baz");
                    REQUIRE(std::string_view{info->svalue[1]} == "MYIOR");
                    REQUIRE(std::string_view{info->svalue[2]} == "version");
                    REQUIRE(std::string_view{info->svalue[3]} == "exe/inst");
                    REQUIRE(std::string_view{info->svalue[4]} == "hostname");
                    REQUIRE(std::string_view{info->svalue[5]} != "?"); // start time
                    REQUIRE(std::string_view{info->svalue[6]} == "?"); // stop time
                    REQUIRE(std::string_view{info->svalue[7]} == "device_class");

                    REQUIRE(info->lvalue[0] == 1);     // exported
                    REQUIRE(info->lvalue[1] == 12345); // pid
                }
            }

            AND_WHEN("We unexport the device")
            {
                Tango::DevString in = Tango::string_dup("foo/bar/baz");
                backend.unexport_device(in);
                Tango::string_free(in);

                THEN("We have a DeviceRecord which is not exported")
                {
                    using Catch::Matchers::SizeIs;

                    const auto &tables = backend.runtime_tables();
                    const auto &table = tables.get_table<FileDb::DeviceRecord>();

                    REQUIRE_THAT(table, SizeIs(1));

                    REQUIRE(table[0].ident == "foo/bar/baz");
                    REQUIRE(!table[0].exported);
                    REQUIRE(table[0].server == "exe/inst");
                    REQUIRE(table[0].device_class == "device_class");
                    REQUIRE(table[0].ior == "MYIOR");
                    REQUIRE(table[0].host == "hostname");
                    REQUIRE(table[0].pid == 12345);
                    REQUIRE(table[0].started != std::nullopt);
                    REQUIRE(table[0].stopped != std::nullopt);
                }
            }
        }
    }
}

SCENARIO("Unexport server")
{
    GIVEN("A FileDB backend with a couple of exported devices defined with the same server")
    {
        FileDb::DbRuntimeTables tables;

        tables.put_record(FileDb::DeviceRecord{"foo/bar/baz", "exe/inst1", "device_class", true});
        tables.put_record(FileDb::DeviceRecord{"foo/bar/qux", "exe/inst1", "device_class", true});
        tables.put_record(FileDb::DeviceRecord{"foo/bar/quux", "exe/inst2", "device_class", true});

        FileDb::FileDbDeviceBackend backend{std::move(tables)};

        WHEN("We unexport the server")
        {
            Tango::DevString in = Tango::string_dup("exe/inst1");
            backend.unexport_server(in);
            Tango::string_free(in);

            THEN("We have DeviceRecords with the devices unexported")
            {
                using Catch::Matchers::SizeIs;

                const auto &tables = backend.runtime_tables();
                const auto &table = tables.get_table<FileDb::DeviceRecord>();

                REQUIRE_THAT(table, SizeIs(3));

                {
                    auto found = std::find_if(
                        table.begin(), table.end(), [](const auto &r) { return r.ident == "foo/bar/baz"; });

                    REQUIRE(found != table.end());
                    REQUIRE(!found->exported);
                }

                {
                    auto found = std::find_if(
                        table.begin(), table.end(), [](const auto &r) { return r.ident == "foo/bar/qux"; });

                    REQUIRE(found != table.end());
                    REQUIRE(!found->exported);
                }

                {
                    auto found = std::find_if(
                        table.begin(), table.end(), [](const auto &r) { return r.ident == "foo/bar/quux"; });

                    REQUIRE(found != table.end());
                    REQUIRE(found->exported);
                }
            }
        }
    }
}

SCENARIO("Delete server")
{
    GIVEN("A FileDB backend with a server and device records")
    {
        FileDb::DbRuntimeTables runtime_tables;
        FileDb::DbConfigTables config_tables;

        config_tables.put_record(FileDb::ServerRecord{"exe/inst1/device_class", {"foo/bar/baz", "foo/bar/qux"}});
        config_tables.put_record(FileDb::ServerRecord{"exe/inst2/device_class", {"foo/bar/quux"}});

        runtime_tables.put_record(FileDb::DeviceRecord{"foo/bar/baz", "exe/inst1", "device_class", true});
        runtime_tables.put_record(FileDb::DeviceRecord{"foo/bar/qux", "exe/inst1", "device_class", true});
        runtime_tables.put_record(FileDb::DeviceRecord{"foo/bar/quux", "exe/inst2", "device_class", true});

        FileDb::FileDbDeviceBackend backend{std::move(config_tables), std::move(runtime_tables)};

        WHEN("We delete a server")
        {
            Tango::DevString in = Tango::string_dup("exe/inst1");
            backend.delete_server(in);
            Tango::string_free(in);

            THEN("We only have one ServerRecord")
            {
                using Catch::Matchers::SizeIs;

                const auto &tables = backend.config_tables();
                const auto &table = tables.get_table<FileDb::ServerRecord>();

                REQUIRE_THAT(table, SizeIs(1));

                REQUIRE(table[0].server() == "exe/inst2");
                REQUIRE(table[0].device_class() == "device_class");

                AND_THEN("We only have one DeviceRecord")
                {
                    const auto &tables = backend.runtime_tables();
                    const auto &table = tables.get_table<FileDb::DeviceRecord>();

                    REQUIRE_THAT(table, SizeIs(1));
                    REQUIRE(table[0].device() == "foo/bar/quux");
                }
            }
        }
    }
}

SCENARIO("Get class list")
{
    GIVEN("A FileDB backend with a server and device records")
    {
        FileDb::DbConfigTables config_tables;
        FileDb::DbRuntimeTables runtime_tables;

        config_tables.put_record(FileDb::ServerRecord{"exe/inst1/device_class1", {"foo/bar/baz"}});
        config_tables.put_record(FileDb::ServerRecord{"exe/inst2/device_class1", {"foo/bar/qux"}});
        config_tables.put_record(FileDb::ServerRecord{"exe/inst2/device_class2", {"foo/bar/quux"}});
        config_tables.put_record(FileDb::ServerRecord{"exe/inst3/device_class2", {"foo/bar/quuux"}});
        config_tables.put_record(FileDb::ServerRecord{"exe/inst3/other_class", {"foo/bar/quuuux"}});

        FileDb::FileDbDeviceBackend backend{std::move(config_tables), std::move(runtime_tables)};

        WHEN("we query for all classes")
        {
            Tango::DevString_var in = Tango::string_dup("*");
            Tango::DevVarStringArray_var classes = backend.get_class_list(in);

            THEN("we find each class once")
            {
                using namespace Catch::Matchers;

                REQUIRE(classes->length() == 3);
                REQUIRE_THAT(classes, Contains(std::string_view{"device_class1"}));
                REQUIRE_THAT(classes, Contains(std::string_view{"device_class2"}));
                REQUIRE_THAT(classes, Contains(std::string_view{"other_class"}));
            }
        }
    }
}
