/*
 * (C) Copyright 1996- ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation nor
 * does it submit to any jurisdiction.
 */

#include <cstring>
#include <memory>

#include "eckit/config/Resource.h"
#include "eckit/config/YAMLConfiguration.h"
#include "eckit/filesystem/PathName.h"
#include "eckit/filesystem/TmpDir.h"
#include "eckit/filesystem/TmpFile.h"
#include "eckit/filesystem/URI.h"
#include "eckit/io/FileHandle.h"
#include "eckit/io/MemoryHandle.h"
#include "eckit/testing/Filesystem.h"
#include "eckit/testing/Test.h"

#include "metkit/mars/MarsRequest.h"

#include "fdb5/api/FDB.h"
#include "fdb5/api/helpers/FDBToolRequest.h"
#include "fdb5/api/helpers/WipeIterator.h"
#include "fdb5/api/local/WipeVisitor.h"
#include "fdb5/config/Config.h"
#include "fdb5/fdb5_config.h"

#include "fdb5/toc/TocStore.h"

#include "fdb5/daos/DaosArrayPartHandle.h"
#include "fdb5/daos/DaosPool.h"
#include "fdb5/daos/DaosSession.h"

#include "fdb5/daos/DaosCatalogueReader.h"
#include "fdb5/daos/DaosCatalogueWriter.h"
#include "fdb5/daos/DaosFieldLocation.h"
#include "fdb5/daos/DaosStore.h"

using namespace eckit::testing;
using namespace eckit;

#ifdef fdb5_HAVE_DUMMY_DAOS
eckit::PathName& tmp_dummy_daos_root() {
    static eckit::PathName d("./daos_catalogue_tests_dummy_daos_root");
    return d;
}
#endif

// temporary schema,spaces,root files common to all DAOS Catalogue tests

eckit::TmpFile& schema_file() {
    static eckit::TmpFile f{};
    return f;
}

eckit::TmpFile& opt_schema_file() {
    static eckit::TmpFile f{};
    return f;
}

eckit::PathName& catalogue_tests_tmp_root() {
    static eckit::PathName cd("./daos_catalogue_tests_fdb_root");
    return cd;
}

size_t countWipeable(fdb5::WipeIterator& wipeObject, bool print = true) {
    size_t count = 0;
    fdb5::WipeElement elem;
    while (wipeObject.next(elem)) {
        if (print) {
            std::cout << elem << std::endl;
        }
        if (elem.type() != fdb5::WipeElementType::ERROR && elem.type() != fdb5::WipeElementType::CATALOGUE_INFO &&
            elem.type() != fdb5::WipeElementType::CATALOGUE_SAFE && elem.type() != fdb5::WipeElementType::STORE_SAFE) {
            count += elem.uris().size();
        }
    }
    return count;
}

namespace fdb {
namespace test {

CASE("DaosCatalogue tests") {

    // setup

#ifdef fdb5_HAVE_DUMMY_DAOS
    if (tmp_dummy_daos_root().exists()) {
        testing::deldir(tmp_dummy_daos_root());
    }
    tmp_dummy_daos_root().mkdir();
    ::setenv("DUMMY_DAOS_DATA_ROOT", tmp_dummy_daos_root().path().c_str(), 1);
#endif

    // ensure fdb root directory exists. If not, then that root is
    // registered as non existing and Catalogue/Store tests fail.
    if (catalogue_tests_tmp_root().exists()) {
        testing::deldir(catalogue_tests_tmp_root());
    }
    catalogue_tests_tmp_root().mkdir();
    ::setenv("FDB_ROOT_DIRECTORY", catalogue_tests_tmp_root().path().c_str(), 1);

    // prepare schema for tests involving DaosCatalogue

    std::string schema_str{"[ a, b [ c, d [ e, f ]]]"};

    std::unique_ptr<eckit::DataHandle> hs(schema_file().fileHandle());
    hs->openForWrite(schema_str.size());
    {
        eckit::AutoClose closer(*hs);
        hs->write(schema_str.data(), schema_str.size());
    }

    std::string opt_schema_str{"[ a, b [ c?, d [ e?, f ]]]"};

    std::unique_ptr<eckit::DataHandle> hs_opt(opt_schema_file().fileHandle());
    hs_opt->openForWrite(opt_schema_str.size());
    {
        eckit::AutoClose closer(*hs_opt);
        hs_opt->write(opt_schema_str.data(), opt_schema_str.size());
    }

    // this is necessary to avoid ~fdb/etc/fdb/schema being used where
    // LibFdb5::instance().defaultConfig().schema() is called
    // due to no specified schema file (e.g. in Key::registry())
    ::setenv("FDB_SCHEMA_FILE", schema_file().path().c_str(), 1);

    // test parameters

    std::string root_cont_name{"test_root"};
    int container_oids_per_alloc = 1000;
#if defined(fdb5_HAVE_DAOS_ADMIN) || defined(fdb5_HAVE_DUMMY_DAOS)
    std::string pool_name{"fdb_pool2"};
#else
    std::string pool_name;
    pool_name = eckit::Resource<std::string>("fdbDaosTestPool;$FDB_DAOS_TEST_POOL", pool_name);
    EXPECT(pool_name.length() > 0);
#endif

    // bootstrap daos

    fdb5::UUID pool_uuid;
    {
        /// @btodo: should DaosManager really be configured here? May be better not to configure it
        ///         here, only specify client config in FDB config file, and let the constructor of
        ///         DaosStore or DaosCatalogue configure DaosManager.
        fdb5::DaosManager::instance().configure(eckit::LocalConfiguration(
            YAMLConfiguration("container_oids_per_alloc: " + std::to_string(container_oids_per_alloc))));
        fdb5::DaosSession s{};

#ifdef fdb5_HAVE_DAOS_ADMIN
        fdb5::DaosPool& pool = s.createPool(pool_name);
#else
#ifdef fdb5_HAVE_DUMMY_DAOS
        std::string pool_uuid_str{"00000000-0000-0000-0000-000000000004"};
        (tmp_dummy_daos_root() / pool_uuid_str).mkdir();
        ::symlink((tmp_dummy_daos_root() / pool_uuid_str).path().c_str(),
                  (tmp_dummy_daos_root() / pool_name).path().c_str());
#endif
        fdb5::DaosPool& pool = s.getPool(pool_name);
#endif

        pool_uuid = pool.uuid();
    }

    SECTION("DaosCatalogue archive (index) and retrieve without a Store") {

        std::string config_str{
            "spaces:\n"
            "- roots:\n"
            "  - path: " +
            catalogue_tests_tmp_root().asString() +
            "\n"
            "schema : " +
            schema_file().path() +
            "\n"
            "daos:\n"
            "  catalogue:\n"
            "    pool: " +
            pool_name +
            "\n"
            "    root_cont: " +
            root_cont_name +
            "\n"
            "  client:\n"
            "    container_oids_per_alloc: " +
            std::to_string(container_oids_per_alloc)};

        fdb5::Config config{YAMLConfiguration(config_str)};
        fdb5::Schema schema{schema_file()};

        /// @note: a=11,b=22 instead of a=1,b=2 to avoid collision with potential parallel runs of store tests using
        /// a=1,b=2
        fdb5::Key request_key({{"a", "11"}, {"b", "22"}, {"c", "3"}, {"d", "4"}, {"e", "5"}, {"f", "6"}});
        fdb5::Key db_key({{"a", "11"}, {"b", "22"}});
        fdb5::Key index_key({{"c", "3"}, {"d", "4"}});
        fdb5::Key field_key({{"e", "5"}, {"f", "6"}});

        // archive

        /// DaosManager is configured with client config from the file
        auto loc = std::make_unique<const fdb5::DaosFieldLocation>(eckit::URI{"daos", "test_uri"}, eckit::Offset(0),
                                                                   eckit::Length(1), fdb5::Key());

        {
            fdb5::DaosCatalogueWriter dcatw{db_key, config};

            fdb5::DaosName db_cont{pool_name, db_key.valuesToString()};
            fdb5::DaosKeyValueOID cat_kv_oid{0, 0, OC_S1};  /// @todo: take oclass from config
            fdb5::DaosKeyValueName cat_kv{pool_name, db_key.valuesToString(), cat_kv_oid};
            EXPECT(db_cont.exists());
            EXPECT(cat_kv.exists());

            fdb5::Catalogue& cat = dcatw;
            cat.selectIndex(index_key);
            fdb5::DaosKeyValueOID index_kv_oid{index_key.valuesToString(), OC_S1};  /// @todo: take oclass from config
            fdb5::DaosKeyValueName index_kv{pool_name, db_key.valuesToString(), index_kv_oid};
            EXPECT(index_kv.exists());
            EXPECT(cat_kv.has(index_key.valuesToString()));

            fdb5::CatalogueWriter& catw = dcatw;
            catw.archive(index_key, field_key, std::move(loc));
            EXPECT(index_kv.has(field_key.valuesToString()));
            fdb5::DaosKeyValueOID e_axis_kv_oid{index_key.valuesToString() + std::string{".e"}, OC_S1};
            fdb5::DaosKeyValueName e_axis_kv{pool_name, db_key.valuesToString(), e_axis_kv_oid};
            EXPECT(e_axis_kv.exists());
            EXPECT(e_axis_kv.has("5"));
            fdb5::DaosKeyValueOID f_axis_kv_oid{index_key.valuesToString() + std::string{".f"}, OC_S1};
            fdb5::DaosKeyValueName f_axis_kv{pool_name, db_key.valuesToString(), f_axis_kv_oid};
            EXPECT(f_axis_kv.exists());
            EXPECT(f_axis_kv.has("6"));
        }

        // retrieve

        {
            fdb5::DaosCatalogueReader dcatr{db_key, config};

            fdb5::Catalogue& cat = dcatr;
            cat.selectIndex(index_key);

            fdb5::Field f;
            fdb5::CatalogueReader& catr = dcatr;
            catr.retrieve(field_key, f);
            EXPECT_EQUAL(f.location().uri().name(), eckit::URI("daos", "test_uri").name());
            EXPECT_EQUAL(f.location().offset(), eckit::Offset(0));
            EXPECT_EQUAL(f.location().length(), eckit::Length(1));
        }

        // remove (manual deindex)

        {
            fdb5::DaosCatalogueWriter dcatw{db_key, config};
            fdb5::DaosName db_cont{dcatw.uri()};
            std::ostream out(std::cout.rdbuf());

            fdb5::DaosCatalogue::remove(db_cont, out, out, true);

            fdb5::DaosKeyValueOID cat_kv_oid{0, 0, OC_S1};  /// @todo: take oclass from config
            fdb5::DaosKeyValueName cat_kv{pool_name, db_key.valuesToString(), cat_kv_oid};
            EXPECT_NOT(cat_kv.exists());
            EXPECT_NOT(db_cont.exists());
        }
    }

    SECTION("DaosCatalogue archive (index) and retrieve with a DaosStore") {

        // FDB configuration

        std::string config_str{
            "spaces:\n"
            "- roots:\n"
            "  - path: " +
            catalogue_tests_tmp_root().asString() +
            "\n"
            "schema : " +
            schema_file().path() +
            "\n"
            "daos:\n"
            "  store:\n"
            "    pool: " +
            pool_name +
            "\n"
            "  catalogue:\n"
            "    pool: " +
            pool_name +
            "\n"
            "    root_cont: " +
            root_cont_name +
            "\n"
            "  client:\n"
            "    container_oids_per_alloc: " +
            std::to_string(container_oids_per_alloc)};

        fdb5::Config config{YAMLConfiguration(config_str)};

        // schema

        fdb5::Schema schema{schema_file()};

        // request

        fdb5::Key request_key({{"a", "11"}, {"b", "22"}, {"c", "3"}, {"d", "4"}, {"e", "5"}, {"f", "6"}});
        fdb5::Key db_key({{"a", "11"}, {"b", "22"}});
        fdb5::Key index_key({{"c", "3"}, {"d", "4"}});
        fdb5::Key field_key({{"e", "5"}, {"f", "6"}});

        // store data

        char data[] = "test";

        fdb5::DaosStore dstore{db_key, config};
        fdb5::Store& store = static_cast<fdb5::Store&>(dstore);
        std::unique_ptr<const fdb5::FieldLocation> loc(store.archive(index_key, data, sizeof(data)));
        /// @todo: there are two cont create with label here
        /// @todo: again, daos_fini happening before cont and pool close

        // index data

        {
            fdb5::DaosCatalogueWriter dcatw{db_key, config};
            fdb5::Catalogue& cat = dcatw;
            cat.deselectIndex();
            cat.selectIndex(index_key);
            fdb5::CatalogueWriter& catw = dcatw;
            catw.archive(index_key, field_key, std::move(loc));

            /// flush store before flushing catalogue
            dstore.flush();  // not necessary if using a DAOS store
        }

        // find data

        fdb5::Field field;
        {
            fdb5::DaosCatalogueReader dcatr{db_key, config};
            fdb5::Catalogue& cat = dcatr;
            cat.selectIndex(index_key);
            fdb5::CatalogueReader& catr = dcatr;
            catr.retrieve(field_key, field);
        }
        std::cout << "Read location: " << field.location() << std::endl;

        // retrieve data

        std::unique_ptr<eckit::DataHandle> dh(store.retrieve(field));
        EXPECT(dynamic_cast<fdb5::DaosArrayPartHandle*>(dh.get()));

        eckit::MemoryHandle mh;
        dh->copyTo(mh);
        EXPECT_EQUAL(mh.size(), eckit::Length(sizeof(data)));
        EXPECT_EQUAL(::memcmp(mh.data(), data, sizeof(data)), 0);

        // deindex data

        {
            fdb5::DaosCatalogueWriter dcat{db_key, config};
            fdb5::Catalogue& cat = static_cast<fdb5::Catalogue&>(dcat);
            eckit::Queue<fdb5::CatalogueWipeState> queue(100);
            metkit::mars::MarsRequest r = db_key.request("retrieve");
            fdb5::api::local::WipeCatalogueVisitor wv{queue, r, true};
            cat.visitEntries(wv, false);
        }

        /// @todo: again, daos_fini happening before
    }

    SECTION("DaosCatalogue archive (index) and retrieve with a TocStore") {

        // FDB configuration

        std::string config_str{
            "spaces:\n"
            "- roots:\n"
            "  - path: " +
            catalogue_tests_tmp_root().asString() +
            "\n"
            "schema : " +
            schema_file().path() +
            "\n"
            "daos:\n"
            "  catalogue:\n"
            "    pool: " +
            pool_name +
            "\n"
            "    root_cont: " +
            root_cont_name +
            "\n"
            "  client:\n"
            "    container_oids_per_alloc: " +
            std::to_string(container_oids_per_alloc)};

        fdb5::Config config{YAMLConfiguration(config_str)};

        // schema

        fdb5::Schema schema{schema_file()};

        // request

        fdb5::Key request_key({{"a", "11"}, {"b", "22"}, {"c", "3"}, {"d", "4"}, {"e", "5"}, {"f", "6"}});
        fdb5::Key db_key({{"a", "11"}, {"b", "22"}});
        fdb5::Key index_key({{"c", "3"}, {"d", "4"}});
        fdb5::Key field_key({{"e", "5"}, {"f", "6"}});

        // store data

        char data[] = "test";

        fdb5::TocStore tstore{db_key, config};
        fdb5::Store& store = static_cast<fdb5::Store&>(tstore);
        std::unique_ptr<const fdb5::FieldLocation> loc(store.archive(index_key, data, sizeof(data)));
        /// @todo: there are two cont create with label here
        /// @todo: again, daos_fini happening before cont and pool close

        // index data

        {
            fdb5::DaosCatalogueWriter dcatw{db_key, config};
            fdb5::Catalogue& cat = dcatw;
            cat.deselectIndex();
            cat.selectIndex(index_key);
            fdb5::CatalogueWriter& catw = dcatw;
            catw.archive(index_key, field_key, std::move(loc));

            /// flush store before flushing catalogue
            tstore.flush();
        }

        // find data

        fdb5::Field field;
        {
            fdb5::DaosCatalogueReader dcatr{db_key, config};
            fdb5::Catalogue& cat = dcatr;
            cat.selectIndex(index_key);
            fdb5::CatalogueReader& catr = dcatr;
            catr.retrieve(field_key, field);
        }
        std::cout << "Read location: " << field.location() << std::endl;

        // retrieve data

        std::unique_ptr<eckit::DataHandle> dh(store.retrieve(field));

        std::vector<char> test(dh->size());
        dh->openForRead();
        {
            eckit::AutoClose closer(*dh);
            dh->read(&test[0], test.size() - 3);
        }
        eckit::MemoryHandle mh;
        dh->copyTo(mh);
        EXPECT_EQUAL(mh.size(), eckit::Length(sizeof(data)));
        EXPECT_EQUAL(::memcmp(mh.data(), data, sizeof(data)), 0);

        // remove data

        /// @todo: should DaosStore::remove accept full URIs to field arrays and remove the store container?
        eckit::PathName store_path{field.location().uri().path()};
        std::ostream out(std::cout.rdbuf());
        store.remove(field.location().uri(), out, out, false);
        EXPECT(store_path.exists());
        store.remove(field.location().uri(), out, out, true);
        EXPECT_NOT(store_path.exists());

        // deindex data

        {
            fdb5::DaosCatalogueWriter dcat{db_key, config};
            fdb5::Catalogue& cat = static_cast<fdb5::Catalogue&>(dcat);
            eckit::Queue<fdb5::CatalogueWipeState> queue(100);
            metkit::mars::MarsRequest r = db_key.request("retrieve");
            fdb5::api::local::WipeCatalogueVisitor wv{queue, r, true};
            cat.visitEntries(wv, false);
        }

        /// @todo: again, daos_fini happening before
    }

    SECTION("Via FDB API with a DAOS catalogue and store") {

        // FDB configuration

        int container_oids_per_alloc_small = 100;

        std::string config_str{
            "spaces:\n"
            "- roots:\n"
            "  - path: " +
            catalogue_tests_tmp_root().asString() +
            "\n"
            "type: local\n"
            "schema : " +
            schema_file().path() +
            "\n"
            "engine: daos\n"
            "store: daos\n"
            "daos:\n"
            "  catalogue:\n"
            "    pool: " +
            pool_name +
            "\n"
            "    root_cont: " +
            root_cont_name +
            "\n"
            "  store:\n"
            "    pool: " +
            pool_name +
            "\n"
            "  client:\n"
            "    container_oids_per_alloc: " +
            std::to_string(container_oids_per_alloc_small)};

        fdb5::Config config{YAMLConfiguration(config_str)};

        // request

        fdb5::Key request_key({{"a", "11"}, {"b", "22"}, {"c", "3"}, {"d", "4"}, {"e", "5"}, {"f", "6"}});
        fdb5::Key db_key({{"a", "11"}, {"b", "22"}});
        fdb5::Key index_key({{"a", "11"}, {"b", "22"}, {"c", "3"}, {"d", "4"}});

        fdb5::FDBToolRequest full_req{request_key.request(), false, std::vector<std::string>{"a", "b"}};
        fdb5::FDBToolRequest index_req{index_key.request(), false, std::vector<std::string>{"a", "b"}};
        fdb5::FDBToolRequest db_req{db_key.request(), false, std::vector<std::string>{"a", "b"}};
        fdb5::FDBToolRequest all_req{metkit::mars::MarsRequest{}, true, std::vector<std::string>{}};

        // initialise FDB

        fdb5::FDB fdb(config);

        // check FDB is empty

        size_t count;
        fdb5::ListElement info;

        /// @todo: here, DaosManager is being configured with DAOS client config passed to FDB instance constructor.
        //   It happens in EntryVisitMechanism::visit when calling DB::open. Is this OK, or should this configuring
        //   rather happen as part of transforming a FieldLocation into a DataHandle? It is probably OK. One thing
        //   is to configure the DAOS client and the other thing is to initialise it.
        auto listObject = fdb.list(db_req);

        count = 0;
        while (listObject.next(info)) {
            info.print(std::cout, true, true, false, " ");
            std::cout << std::endl;
            ++count;
        }
        EXPECT_EQUAL(count, 0);

        // archive data

        char data[] = "test";

        /// @todo: here, DaosManager is being reconfigured with identical config, and it happens again multiple times
        /// below.
        //   Should this be avoided?
        fdb.archive(request_key, data, sizeof(data));

        fdb.flush();

        // retrieve data

        metkit::mars::MarsRequest r = request_key.request("retrieve");
        std::unique_ptr<eckit::DataHandle> dh(fdb.retrieve(r));

        eckit::MemoryHandle mh;
        dh->copyTo(mh);
        EXPECT_EQUAL(mh.size(), eckit::Length(sizeof(data)));
        EXPECT_EQUAL(::memcmp(mh.data(), data, sizeof(data)), 0);

        // list all

        listObject = fdb.list(all_req);
        count = 0;
        while (listObject.next(info)) {
            // info.print(std::cout, true, true);
            // std::cout << std::endl;
            count++;
        }
        EXPECT_EQUAL(count, 1);

        // wipe data

        fdb5::WipeElement elem;

        // dry run attempt to wipe with too specific request

        auto wipeObject = fdb.wipe(full_req);
        EXPECT_EQUAL(countWipeable(wipeObject), 0);

        // dry run wipe index and store unit
        wipeObject = fdb.wipe(index_req);
        EXPECT(countWipeable(wipeObject) > 0);

        // dry run wipe database
        wipeObject = fdb.wipe(db_req);
        EXPECT(countWipeable(wipeObject) > 0);

        // ensure field still exists
        listObject = fdb.list(full_req);
        count = 0;
        while (listObject.next(info)) {
            // info.print(std::cout, true, true);
            // std::cout << std::endl;
            count++;
        }
        EXPECT_EQUAL(count, 1);

        // attempt to wipe with too specific request
        wipeObject = fdb.wipe(full_req, true);
        EXPECT_EQUAL(countWipeable(wipeObject), 0);
        /// @todo: really needed?
        fdb.flush();

        // wipe index and store unit
        wipeObject = fdb.wipe(index_req, true);
        EXPECT(countWipeable(wipeObject) > 0);
        /// @todo: really needed?
        fdb.flush();

        // ensure field does not exist
        listObject = fdb.list(full_req);
        count = 0;
        while (listObject.next(info)) {
            count++;
        }
        EXPECT_EQUAL(count, 0);

        /// @todo: ensure index and corresponding container do not exist
        /// @todo: ensure DB still exists
        /// @todo: list db or index and expect count = 0?

        // re-archive data

        /// @note: FDB holds a LocalFDB which holds an Archiver which holds open DBs (DaosCatalogueWriters).
        ///   If a whole DB is wiped, the top-level structures for that DB (main and catalogue KVs in this case)
        ///   are deleted. If willing to archive again into that DB, the DB needs to be constructed again as the
        ///   top-level structures are only generated as part of the DaosCatalogueWriter constructor. There is
        ///   no way currently to destroy the open DBs held by FDB other than entirely destroying FDB.
        ///   Alternatively, a separate FDB instance can be created.
        fdb5::FDB fdb2(config);

        fdb2.archive(request_key, data, sizeof(data));

        fdb2.flush();

        listObject = fdb2.list(full_req);
        count = 0;
        while (listObject.next(info)) {
            // info.print(std::cout, true, true);
            // std::cout << std::endl;
            count++;
        }
        EXPECT_EQUAL(count, 1);

        // wipe full database

        wipeObject = fdb2.wipe(db_req, true);
        EXPECT(countWipeable(wipeObject) > 0);
        /// @todo: really needed?
        fdb2.flush();

        // ensure field does not exist

        listObject = fdb2.list(full_req);
        count = 0;
        while (listObject.next(info)) {
            // info.print(std::cout, true, true);
            // std::cout << std::endl;
            count++;
        }
        EXPECT_EQUAL(count, 0);

        /// @todo: ensure DB and corresponding pool do not exist

        /// @todo: ensure new DaosSession has updated daos client config
    }

    SECTION("OPTIONAL SCHEMA KEYS") {

        // FDB configuration

        ::setenv("FDB_SCHEMA_FILE", opt_schema_file().path().c_str(), 1);

        std::string config_str{
            "spaces:\n"
            "- roots:\n"
            "  - path: " +
            catalogue_tests_tmp_root().asString() +
            "\n"
            "type: local\n"
            "schema : " +
            opt_schema_file().path() +
            "\n"
            "engine: daos\n"
            "store: daos\n"
            "daos:\n"
            "  catalogue:\n"
            "    pool: " +
            pool_name +
            "\n"
            "    root_cont: " +
            root_cont_name +
            "\n"
            "  store:\n"
            "    pool: " +
            pool_name +
            "\n"
            "  client:\n"
            "    container_oids_per_alloc: " +
            std::to_string(container_oids_per_alloc)};

        fdb5::Config config{YAMLConfiguration(config_str)};

        // request

        fdb5::Key request_key({{"a", "11"}, {"b", "22"}, {"d", "4"}, {"f", "6"}});
        fdb5::Key request_key2({{"a", "11"}, {"b", "22"}, {"d", "4"}, {"e", "5"}, {"f", "6"}});
        fdb5::Key db_key({{"a", "11"}, {"b", "22"}});
        fdb5::Key index_key({{"a", "11"}, {"b", "22"}, {"d", "4"}});

        fdb5::FDBToolRequest full_req{request_key.request("retrieve"), false, std::vector<std::string>{"a", "b"}};
        fdb5::FDBToolRequest full_req2{request_key2.request("retrieve"), false, std::vector<std::string>{"a", "b"}};
        fdb5::FDBToolRequest index_req{index_key.request("retrieve"), false, std::vector<std::string>{"a", "b"}};
        fdb5::FDBToolRequest db_req{db_key.request("retrieve"), false, std::vector<std::string>{"a", "b"}};
        fdb5::FDBToolRequest all_req{metkit::mars::MarsRequest{}, true, std::vector<std::string>{}};

        // initialise FDB

        fdb5::FDB fdb(config);

        // check FDB is empty

        size_t count;
        fdb5::ListElement info;

        auto listObject = fdb.list(db_req);

        count = 0;
        while (listObject.next(info)) {
            info.print(std::cout, true, true, false, " ");
            std::cout << std::endl;
            ++count;
        }
        EXPECT_EQUAL(count, 0);

        // archive data with incomplete key

        char data[] = "test";

        fdb.archive(request_key, data, sizeof(data));

        fdb.flush();

        // list data

        listObject = fdb.list(db_req);

        count = 0;
        while (listObject.next(info)) {
            info.print(std::cout, true, true, false, " ");
            std::cout << std::endl;
            ++count;
        }
        EXPECT_EQUAL(count, 1);

        // retrieve data

        {
            metkit::mars::MarsRequest r = request_key.request("retrieve");
            std::unique_ptr<eckit::DataHandle> dh(fdb.retrieve(r));

            eckit::MemoryHandle mh;
            dh->copyTo(mh);
            EXPECT_EQUAL(mh.size(), eckit::Length(sizeof(data)));
            EXPECT_EQUAL(::memcmp(mh.data(), data, sizeof(data)), 0);
        }

        // archive data with complete key

        char data2[] = "abcd";

        fdb.archive(request_key2, data2, sizeof(data));

        fdb.flush();

        // list data

        listObject = fdb.list(db_req);

        count = 0;
        while (listObject.next(info)) {
            info.print(std::cout, true, true, false, " ");
            std::cout << std::endl;
            ++count;
        }
        EXPECT_EQUAL(count, 2);

        // retrieve data

        {
            metkit::mars::MarsRequest r = request_key.request("retrieve");
            std::unique_ptr<eckit::DataHandle> dh(fdb.retrieve(r));

            eckit::MemoryHandle mh;
            dh->copyTo(mh);
            EXPECT_EQUAL(mh.size(), eckit::Length(sizeof(data)));
            EXPECT_EQUAL(::memcmp(mh.data(), data, sizeof(data)), 0);
        }

        {
            metkit::mars::MarsRequest r = request_key2.request("retrieve");
            std::unique_ptr<eckit::DataHandle> dh(fdb.retrieve(r));

            eckit::MemoryHandle mh;
            dh->copyTo(mh);
            EXPECT_EQUAL(mh.size(), eckit::Length(sizeof(data2)));
            EXPECT_EQUAL(::memcmp(mh.data(), data2, sizeof(data2)), 0);
        }
    }

    // teardown daos

#if defined(fdb5_HAVE_DAOS_ADMIN) || defined(fdb5_HAVE_DUMMY_DAOS)
    /// AutoPoolDestroy is not possible here because the pool is
    /// created above with an ephemeral session
    fdb5::DaosSession().destroyPool(pool_uuid);
#else
    for (auto& c : fdb5::DaosSession().getPool(pool_uuid).listContainers()) {
        if (c == root_cont_name || c == "11:22") {
            fdb5::DaosSession().getPool(pool_uuid).destroyContainer(c);
        }
    }
#endif

    // remove root directory

    testing::deldir(catalogue_tests_tmp_root());

    // remove dummy daos root

#ifdef fdb5_HAVE_DUMMY_DAOS
    testing::deldir(tmp_dummy_daos_root());
#endif
}

}  // namespace test
}  // namespace fdb

int main(int argc, char** argv) {
    return run_tests(argc, argv);
}
