code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "model/cluster.h"
#include "model/misc/sequencer.h"
#include "model/diff/load/blocks.h"
#include "model/diff/load/devices.h"
#include "model/diff/load/ignored_devices.h"
#include "model/diff/load/ignored_folders.h"
#include "model/diff/load/file_infos.h"
#include "model/diff/load/folder_infos.h"
#include "model/diff/load/folders.h"
#include "model/diff/load/pending_devices.h"
#include "db/prefix.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("loading cluster (base)", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    CHECK(cluster);

    auto self_key = my_device->get_key();
    auto self_data = my_device->serialize();

    SECTION("local device") {
        diff::load::container_t devices;
        devices.emplace_back(diff::load::pair_t{self_key, self_data});
        auto diff = diff::cluster_diff_ptr_t(new diff::load::devices_t(devices));
        REQUIRE(diff->apply(*cluster, get_apply_controller()));
        auto &devices_map = cluster->get_devices();
        REQUIRE(devices_map.size() == 1);
        auto self = devices_map.by_key(self_key);
        auto self_2 = devices_map.by_sha256(my_id.get_sha256());
        REQUIRE(self);
        REQUIRE(self_2);
        CHECK(self == my_device);
        CHECK(self_2 == self);
    }

    SECTION("devices") {
        auto prefix = (char)db::prefix::device;
        auto device_id = test::device_id2sha256("KUEQE66-JJ7P6AD-BEHD4ZW-GPBNW6Q-Y4C3K4Y-X44WJWZ-DVPIDXS-UDRJMA7");
        auto key = utils::bytes_t(device_id.size() + 1);
        key[0] = prefix;
        std::copy(device_id.begin(), device_id.end(), &key[1]);
        db::Device db_device;
        db::set_cert_name(db_device, "cn");
        db::set_name(db_device, "peer-name");

        device_ptr_t peer;

        SECTION("directly") { peer = device_t::create(key, db_device).value(); }

        SECTION("via diff (+ local device)") {
            auto data = db::encode(db_device);
            diff::load::container_t devices;
            devices.emplace_back(diff::load::pair_t{key, data});
            devices.emplace_back(diff::load::pair_t{self_key, self_data});
            auto diff = diff::cluster_diff_ptr_t(new diff::load::devices_t(devices));
            REQUIRE(diff->apply(*cluster, get_apply_controller()));
            auto &devices_map = cluster->get_devices();
            REQUIRE(devices_map.size() == 2);
            peer = devices_map.by_key(key);
            auto peer_2 = devices_map.by_sha256(device_id);
            CHECK(peer_2 == peer);

            auto self = devices_map.by_key(my_device->get_key());
            auto self_2 = devices_map.by_sha256(my_id.get_sha256());
            CHECK(self == my_device);
            CHECK(self_2 == self);
        }

        REQUIRE(peer);
        CHECK(peer->get_name() == "peer-name");
        CHECK(peer->get_cert_name());
        CHECK(peer->get_cert_name().value() == "cn");
    }

    SECTION("blocks") {
        auto hash = utils::sha256_digest(as_bytes("12345")).value();
        auto bi = proto::BlockInfo();
        proto::set_size(bi, 5);
        proto::set_hash(bi, hash);

        auto block = block_info_t::create(bi).assume_value();
        auto key = block->get_key();

        auto target_block = block_info_ptr_t();

        SECTION("via diff") {
            auto data = block->serialize();
            diff::load::container_t blocks;
            blocks.emplace_back(diff::load::pair_t{key, data});
            auto diff = diff::cluster_diff_ptr_t(new diff::load::blocks_t(blocks));
            REQUIRE(diff->apply(*cluster, get_apply_controller()));
            auto &blocks_map = cluster->get_blocks();
            REQUIRE(blocks_map.size() == 1);
            target_block = blocks_map.by_hash(hash);
        }

        REQUIRE(target_block);
        CHECK(target_block->get_hash() == block->get_hash());
        CHECK(target_block->get_key() == block->get_key());
        CHECK(target_block->get_weak_hash() == block->get_weak_hash());
        CHECK(target_block->get_size() == block->get_size());
    }

    SECTION("ignored_devices") {
        db::SomeDevice db_device;
        db::set_name(db_device, "my-label");
        db::set_address(db_device, "tcp://127.0.0.1");

        auto device_id =
            device_id_t::from_string("KUEQE66-JJ7P6AD-BEHD4ZW-GPBNW6Q-Y4C3K4Y-X44WJWZ-DVPIDXS-UDRJMA7").value();

        auto id = ignored_device_t::create(device_id, db_device).value();
        auto key = id->get_key();
        auto data = id->serialize();

        SECTION("via diff") {
            diff::load::container_t devices;
            devices.emplace_back(diff::load::pair_t{key, data});
            auto diff = diff::cluster_diff_ptr_t(new diff::load::ignored_devices_t(devices));
            REQUIRE(diff->apply(*cluster, get_apply_controller()));
            auto &map = cluster->get_ignored_devices();
            REQUIRE(map.size() == 1);

            auto target = map.by_sha256(device_id.get_sha256());
            REQUIRE(target);
            CHECK(target->get_device_id() == id->get_device_id());
            CHECK(target->get_key() == id->get_key());
        }
    }

    SECTION("unknown_devices") {
        db::SomeDevice db_device;
        db::set_name(db_device, "my-label");
        db::set_address(db_device, "tcp://127.0.0.1");

        auto device_id =
            device_id_t::from_string("KUEQE66-JJ7P6AD-BEHD4ZW-GPBNW6Q-Y4C3K4Y-X44WJWZ-DVPIDXS-UDRJMA7").value();

        auto id = pending_device_t::create(device_id, db_device).value();
        auto key = id->get_key();
        auto data = id->serialize();

        SECTION("via diff") {
            diff::load::container_t devices;
            devices.emplace_back(diff::load::pair_t{key, data});
            auto diff = diff::cluster_diff_ptr_t(new diff::load::pending_devices_t(devices));
            REQUIRE(diff->apply(*cluster, get_apply_controller()));
            auto &map = cluster->get_pending_devices();
            REQUIRE(map.size() == 1);

            auto target = map.by_sha256(device_id.get_sha256());
            REQUIRE(target);
            CHECK(target->get_device_id() == id->get_device_id());
            CHECK(target->get_key() == id->get_key());
        }
    }

    SECTION("folders") {
        db::Folder db_folder;
        db::set_id(db_folder, "1234-5678");
        db::set_label(db_folder, "my-label");
        db::set_path(db_folder, "/my/path");

        auto uuid = sequencer->next_uuid();
        auto id = std::string("1234-5678");

        auto folder = folder_t::create(uuid, db_folder).value();

        SECTION("via diff") {
            auto data = db::encode(db_folder);
            diff::load::container_t folders;
            auto key = folder->get_key();
            folders.emplace_back(diff::load::pair_t{key, data});
            auto diff = diff::cluster_diff_ptr_t(new diff::load::folders_t(folders));
            REQUIRE(diff->apply(*cluster, get_apply_controller()));
            auto &map = cluster->get_folders();
            REQUIRE(map.size() == 1);
            auto f1 = map.begin()->item;
            folder = map.by_key(key);
            REQUIRE(folder);
            REQUIRE(folder == map.by_id(id));
            CHECK(folder->get_cluster() == cluster.get());
        }

        REQUIRE(folder);
        CHECK(folder->get_id() == id);
        CHECK(folder->get_label() == "my-label");
        CHECK(folder->get_path() == "/my/path");
    }

    SECTION("ignored folders") {
        db::IgnoredFolder db_folder;
        db::set_label(db_folder, "my-label");

        auto folder = ignored_folder_t::create("folder-id", "my-label").value();
        auto key = folder->get_key();
        auto data = folder->serialize();

        auto target = ignored_folder_ptr_t();

        SECTION("directly") { target = ignored_folder_t::create(key, data).value(); }

        SECTION("via diff") {
            diff::load::container_t folders;
            folders.emplace_back(diff::load::pair_t{key, data});
            auto diff = diff::cluster_diff_ptr_t(new diff::load::ignored_folders_t(folders));
            REQUIRE(diff->apply(*cluster, get_apply_controller()));
            auto &map = cluster->get_ignored_folders();
            REQUIRE(map.size() == 1);
            target = map.by_key(folder->get_id());
        }

        REQUIRE(target);

        CHECK(target->get_id() == folder->get_id());
        CHECK(folder->get_key() == folder->get_key());
        CHECK(folder->get_label() == folder->get_label());
    }
}

TEST_CASE("loading cluster (folder info)", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    CHECK(cluster);
    cluster->get_devices().put(my_device);

    db::Folder db_folder;
    db::set_id(db_folder, "1234-5678");
    db::set_label(db_folder, "my-label");
    db::set_path(db_folder, "/my/path");

    auto uuid = sequencer->next_uuid();
    auto folder = folder_t::create(uuid, db_folder).value();
    cluster->get_folders().put(folder);

    db::FolderInfo db_fi(2, 3, {});
    auto fi = folder_info_t::create(sequencer->next_uuid(), db_fi, my_device, folder).value();
    CHECK(fi);
    CHECK(fi->get_index() == 2ul);
    CHECK(fi->get_max_sequence() == 3ul);

    auto target = folder_info_ptr_t();

    SECTION("via diff") {
        diff::load::container_t folders;
        auto data = fi->serialize();
        folders.emplace_back(diff::load::pair_t{fi->get_key(), data});
        auto diff = diff::cluster_diff_ptr_t(new diff::load::folder_infos_t(folders));
        REQUIRE(diff->apply(*cluster, get_apply_controller()));
        auto &map = folder->get_folder_infos();
        REQUIRE(map.size() == 1);
        target = map.by_uuid(fi->get_uuid());
        REQUIRE(map.by_device(*my_device));
    }

    REQUIRE(target);
    CHECK(target->get_key() == fi->get_key());
    CHECK(target->get_uuid() == fi->get_uuid());
    CHECK(target->get_device() == fi->get_device());
    CHECK(target->get_folder() == fi->get_folder());
    CHECK(target->get_index() == fi->get_index());
    CHECK(target->get_max_sequence() == fi->get_max_sequence());
}

TEST_CASE("loading cluster (file info + block)", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    CHECK(cluster);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto hash = utils::sha256_digest(as_bytes("12345")).value();
    auto bi = proto::BlockInfo();
    proto::set_size(bi, 5);
    proto::set_hash(bi, hash);
    auto block = block_info_t::create(bi).assume_value();
    auto &blocks_map = cluster->get_blocks();
    blocks_map.put(block);

    db::Folder db_folder;
    db::set_id(db_folder, "1234-5678");
    db::set_label(db_folder, "my-label");
    db::set_path(db_folder, "/my/path");

    auto uuid = sequencer->next_uuid();
    auto folder = folder_t::create(uuid, db_folder).value();
    cluster->get_folders().put(folder);

    db::FolderInfo db_folder_info(2, 3, {});
    auto folder_info = folder_info_t::create(sequencer->next_uuid(), db_folder_info, my_device, folder).value();
    CHECK(folder_info);
    CHECK(folder_info->get_index() == 2ul);
    CHECK(folder_info->get_max_sequence() == 3ul);
    folder->get_folder_infos().put(folder_info);

    proto::FileInfo pr_fi;
    proto::set_name(pr_fi, "a/b.txt");
    proto::set_size(pr_fi, 55ul);
    proto::set_block_size(pr_fi, 5ul);
    auto &version = proto::get_version(pr_fi);
    proto::add_counters(version, proto::Counter(my_device->device_id().get_uint(), 0));
    auto fi = file_info_t::create(sequencer->next_uuid(), pr_fi, folder_info).value();
    CHECK(fi);
    for (size_t i = 0; i < 11; ++i) {
        fi->assign_block(block, i);
    }

    auto target = file_info_ptr_t();

    SECTION("directly") {
        auto data = fi->serialize(true);
        auto file_info_db = db::FileInfo();
        REQUIRE(db::decode(data, file_info_db) == 0);
        auto v = db::get_version(file_info_db);
        target = file_info_t::create(fi->get_key(), file_info_db, std::move(folder_info)).value();
        REQUIRE(target);
        CHECK(target->get_size() == 55ul);
        CHECK(target->get_block_size() == 5ul);
        CHECK(target->get_blocks().size() == 11ul);
        CHECK(!target->is_locked());
    }

    SECTION("via diff") {
        diff::load::file_infos_t::container_t container;
        using item_t = decltype(container)::value_type;
        auto db = fi->as_db(true);
        container.emplace_back(item_t{fi->get_key(), std::move(db)});
        auto diff = diff::cluster_diff_ptr_t(new diff::load::file_infos_t(std::move(container)));
        REQUIRE(diff->apply(*cluster, get_apply_controller()));
        auto &map = folder_info->get_file_infos();
        REQUIRE(map.size() == 1);
        target = map.by_uuid(fi->get_uuid());
        REQUIRE(target);
        REQUIRE(map.by_name(fi->get_name()));
        REQUIRE(target->get_blocks().size() == 11);
        REQUIRE(target->get_blocks().begin()->get()->get_hash() == block->get_hash());
        CHECK(!target->is_locked());
    }

    CHECK(target->get_key() == fi->get_key());
    CHECK(target->get_name() == fi->get_name());
    CHECK(target->get_full_name() == fi->get_full_name());
    CHECK(target->get_full_name() == "my-label/a/b.txt");
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "model/cluster.h"
#include "model/misc/sequencer.h"
#include "diff-builder.h"
#include "model/diff/peer/cluster_update.h"
#include "model/diff/cluster_visitor.h"
#include "model/diff/cluster_diff.h"
#include "model/diff/modify/remove_blocks.h"
#include "model/diff/modify/remove_files.h"
#include "model/diff/modify/remove_folder_infos.h"
#include "model/diff/modify/remove_pending_folders.h"
#include "model/diff/modify/upsert_folder_info.h"
#include "model/diff/modify/add_pending_folders.h"
#include "utils/error_code.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

template <typename F> struct my_cluster_update_visitor_t : diff::cluster_visitor_t {
    F fn;
    int remove_blocks = 0;
    int remove_files = 0;
    int remove_folders = 0;
    int remove_pending_folders = 0;
    int add_pending_folders = 0;
    int updated_folders = 0;

    my_cluster_update_visitor_t(F &&fn_) : fn{std::forward<F>(fn_)} {}

    outcome::result<void> operator()(const diff::peer::cluster_update_t &diff, void *custom) noexcept override {
        fn(diff);
        return diff.visit_next(*this, custom);
    }

    outcome::result<void> operator()(const diff::modify::remove_blocks_t &diff, void *custom) noexcept override {
        ++remove_blocks;
        return diff.visit_next(*this, custom);
    }
    outcome::result<void> operator()(const diff::modify::remove_files_t &diff, void *custom) noexcept override {
        ++remove_files;
        return diff.visit_next(*this, custom);
    }
    outcome::result<void> operator()(const diff::modify::remove_folder_infos_t &diff, void *custom) noexcept override {
        ++remove_folders;
        return diff.visit_next(*this, custom);
    }
    outcome::result<void> operator()(const diff::modify::remove_pending_folders_t &diff,
                                     void *custom) noexcept override {
        ++remove_pending_folders;
        return diff.visit_next(*this, custom);
    }
    outcome::result<void> operator()(const diff::modify::upsert_folder_info_t &diff, void *custom) noexcept override {
        ++updated_folders;
        return diff.visit_next(*this, custom);
    }
    outcome::result<void> operator()(const diff::modify::add_pending_folders_t &diff, void *custom) noexcept override {
        ++add_pending_folders;
        return diff.visit_next(*this, custom);
    }
};

TEST_CASE("cluster update, new folder", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    SECTION("unknown folder") {
        auto cc = std::make_unique<proto::ClusterConfig>();
        auto &pr_folder = proto::add_folders(*cc);
        proto::set_id(pr_folder, "some-id");
        proto::set_label(pr_folder, "some-label");

        auto &pr_peer = proto::add_devices(pr_folder);
        proto::set_id(pr_peer, peer_id.get_sha256());
        proto::set_name(pr_peer, peer_device->get_name());
        proto::set_max_sequence(pr_peer, 10);
        proto::set_index_id(pr_peer, 22u);

        auto diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
        REQUIRE(diff_opt);

        auto &diff = diff_opt.value();
        auto r_a = diff->apply(*cluster, get_apply_controller());
        CHECK(r_a);

        auto visitor = my_cluster_update_visitor_t([&](auto &) {});
        auto r_v = diff->visit(visitor, nullptr);
        REQUIRE(r_v);
        REQUIRE(cluster->get_pending_folders().size());
        REQUIRE(visitor.add_pending_folders == 1);
        auto uf = cluster->get_pending_folders().begin()->item;
        CHECK(uf->device_id() == peer_device->device_id());
        CHECK(uf->get_id() == "some-id");
        CHECK(uf->get_max_sequence() == 10);
        CHECK(uf->get_index() == 22ul);

        // no changes
        db::PendingFolder db_pf;
        auto &mf = db::get_folder(db_pf);
        db::set_id(mf, proto::get_id(pr_folder));
        auto &mfi = db::get_folder_info(db_pf);
        db::set_max_sequence(mfi, proto::get_max_sequence(pr_peer));
        db::set_index_id(mfi, proto::get_index_id(pr_peer));

        diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
        REQUIRE(diff_opt);
        diff = diff_opt.value();
        r_a = diff->apply(*cluster, get_apply_controller());
        CHECK(r_a);
        REQUIRE(cluster->get_pending_folders().size() == 1);
        std::ignore = diff->visit(visitor, nullptr);
        REQUIRE(visitor.add_pending_folders == 1);

        // max-id changed
        proto::set_max_sequence(pr_peer, 15);
        diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
        REQUIRE(diff_opt);
        diff = diff_opt.value();
        r_a = diff->apply(*cluster, get_apply_controller());
        CHECK(r_a);
        (void)diff->visit(visitor, nullptr);
        REQUIRE(visitor.add_pending_folders == 2);
        REQUIRE(cluster->get_pending_folders().size() == 1);
        uf = cluster->get_pending_folders().begin()->item;
        CHECK(uf->device_id() == peer_device->device_id());
        CHECK(uf->get_id() == "some-id");
        CHECK(uf->get_max_sequence() == 15);
        CHECK(uf->get_index() == 22ul);
        CHECK(std::distance(cluster->get_pending_folders().begin(), cluster->get_pending_folders().end()) == 1);

        {
            // change unknown folder
            auto cc = std::make_unique<proto::ClusterConfig>();
            auto &pr_folder = proto::add_folders(*cc);
            proto::set_id(pr_folder, "some-id-n");
            proto::set_label(pr_folder, "some-label-N");
            auto &pr_device = proto::add_devices(pr_folder);
            proto::set_id(pr_device, peer_id.get_sha256());
            proto::set_name(pr_device, peer_device->get_name());
            proto::set_max_sequence(pr_device, 10);
            proto::set_index_id(pr_device, 22ul);

            diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
            REQUIRE(diff_opt);

            r_a = diff_opt.value()->apply(*cluster, get_apply_controller());
            CHECK(r_a);

            CHECK(std::distance(cluster->get_pending_folders().begin(), cluster->get_pending_folders().end()) == 1);
        }
    }
    SECTION("existing folder") {
        db::Folder db_folder;
        db::set_id(db_folder, "some-id");
        db::set_label(db_folder, "some-label");
        db::set_path(db_folder, "/my/path");
        auto folder = folder_t::create(sequencer->next_uuid(), db_folder).value();

        cluster->get_folders().put(folder);

        auto folder_info_my = folder_info_ptr_t();
        auto folder_info_peer = folder_info_ptr_t();
        {
            db::FolderInfo db_fi;
            db::set_index_id(db_fi, 5ul);
            db::set_max_sequence(db_fi, 10l);
            folder_info_my = folder_info_t::create(sequencer->next_uuid(), db_fi, my_device, folder).value();
            folder->get_folder_infos().put(folder_info_my);
        }
        {
            db::FolderInfo db_fi;
            db::set_index_id(db_fi, 6ul);
            db::set_max_sequence(db_fi, 10l);
            folder_info_peer = folder_info_t::create(sequencer->next_uuid(), db_fi, peer_device, folder).value();
            folder->get_folder_infos().put(folder_info_peer);
        }

        auto cc = std::make_unique<proto::ClusterConfig>();
        auto &pr_folder = proto::add_folders(*cc);
        proto::set_id(pr_folder, folder->get_id());
        proto::set_label(pr_folder, folder->get_label());
        auto &pr_peer = proto::add_devices(pr_folder);
        proto::set_id(pr_peer, peer_id.get_sha256());
        proto::set_name(pr_peer, peer_device->get_name());
        proto::set_max_sequence(pr_peer, folder_info_peer->get_max_sequence());

        SECTION("nothing changed") {
            proto::set_index_id(pr_peer, folder_info_peer->get_index());

            auto diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
            REQUIRE(diff_opt);

            auto &diff = diff_opt.value();
            auto r_a = diff->apply(*cluster, get_apply_controller());
            CHECK(r_a);
            auto pr_file = proto::FileInfo();
            proto::set_sequence(pr_file, folder_info_peer->get_max_sequence());
            auto &pr_version = proto::get_version(pr_file);
            proto::add_counters(pr_version, proto::Counter(peer_device->device_id().get_uint(), 0));
            auto peer_file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_info_peer).value();
            folder_info_peer->add_strict(peer_file);

            bool visited = false;
            auto visitor = my_cluster_update_visitor_t([&](auto &) { visited = true; });
            auto r_v = diff->visit(visitor, nullptr);
            REQUIRE(r_v);
            REQUIRE(visited);
            REQUIRE(visitor.updated_folders == 0);
        }
    }

    SECTION("reset index") {
        db::Folder db_folder;
        db::set_id(db_folder, "some-id");
        db::set_label(db_folder, "some-label");
        db::set_path(db_folder, "/my/path");
        auto folder = folder_t::create(sequencer->next_uuid(), db_folder).value();

        cluster->get_folders().put(folder);
        auto &folder_infos = folder->get_folder_infos();

        auto folder_info_my = folder_info_ptr_t();
        auto folder_info_peer = folder_info_ptr_t();
        {
            db::FolderInfo db_fi;
            db::set_index_id(db_fi, 5ul);
            db::set_max_sequence(db_fi, 10l);
            folder_info_my = folder_info_t::create(sequencer->next_uuid(), db_fi, my_device, folder).value();
            folder_infos.put(folder_info_my);
        }
        {
            db::FolderInfo db_fi;
            db::set_index_id(db_fi, 6ul);
            db::set_max_sequence(db_fi, 10l);
            folder_info_peer = folder_info_t::create(sequencer->next_uuid(), db_fi, peer_device, folder).value();
            folder_infos.put(folder_info_peer);

            auto pr_file = proto::FileInfo();
            proto::set_name(pr_file, "a.txt");
            proto::set_sequence(pr_file, 10l);
            auto &pr_version = proto::get_version(pr_file);
            proto::add_counters(pr_version, proto::Counter(peer_device->device_id().get_uint(), 0));
            auto file = file_info_t::create(sequencer->next_uuid(), pr_file, folder_info_peer).value();
            folder_info_peer->get_file_infos().put(file);
        }

        auto cc = std::make_unique<proto::ClusterConfig>();
        auto &pr_folder = proto::add_folders(*cc);
        proto::set_id(pr_folder, folder->get_id());
        proto::set_label(pr_folder, folder->get_label());
        auto &pr_peer = proto::add_devices(pr_folder);
        proto::set_id(pr_peer, peer_id.get_sha256());
        proto::set_name(pr_peer, peer_device->get_name());

        SECTION("peer index has changed") {
            SECTION("non-zero index") {
                proto::set_index_id(pr_peer, 1234ul);
                proto::set_max_sequence(pr_peer, 123456u);

                auto diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
                REQUIRE(diff_opt);

                auto &diff = diff_opt.value();
                auto r_a = diff->apply(*cluster, get_apply_controller());
                REQUIRE(r_a);
                auto fi = folder_infos.by_device(*peer_device);
                CHECK(fi->get_index() == 1234ul);
                CHECK(fi->get_max_sequence() == 0);
                CHECK(fi.get() == folder_info_peer.get());

                bool visited = false;
                auto visitor = my_cluster_update_visitor_t([&](auto &) { visited = true; });
                auto r_v = diff->visit(visitor, nullptr);
                REQUIRE(r_v);
                REQUIRE(visited);
                CHECK(visitor.remove_files == 1);
                CHECK(visitor.updated_folders == 1);
                CHECK(visitor.remove_folders == 0);
            }
            SECTION("zero index") {
                proto::set_index_id(pr_peer, 0ul);
                proto::set_max_sequence(pr_peer, 123456u);

                auto diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
                REQUIRE(diff_opt);

                auto &diff = diff_opt.value();
                auto r_a = diff->apply(*cluster, get_apply_controller());
                REQUIRE(r_a);
                auto fi = folder_infos.by_device(*peer_device);
                CHECK(fi->get_index() == 0);
                CHECK(fi->get_max_sequence() == 0);
                CHECK(fi.get() == folder_info_peer.get());

                bool visited = false;
                auto visitor = my_cluster_update_visitor_t([&](auto &) { visited = true; });
                auto r_v = diff->visit(visitor, nullptr);
                REQUIRE(r_v);
                CHECK(visited);
                CHECK(visitor.remove_files == 1);
                CHECK(visitor.updated_folders == 1);
                CHECK(visitor.remove_folders == 0);
            }
        }
    }
}

TEST_CASE("cluster update, reset folder", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    db::Folder db_folder;
    db::set_id(db_folder, "some-id");
    db::set_label(db_folder, "my-label");
    db::set_path(db_folder, "/my/path");
    auto folder = folder_t::create(sequencer->next_uuid(), db_folder).value();

    cluster->get_folders().put(folder);
    auto db_p_folder = [&]() -> db::PendingFolder {
        db::PendingFolder db_p_folder;
        auto &f = db::get_folder(db_p_folder);
        db::set_id(f, "1111-2222");
        db::set_label(f, "unknown");
        return db_p_folder;
    }();
    auto u_folder = pending_folder_t::create(sequencer->next_uuid(), db_p_folder, peer_device->device_id()).value();
    auto &unknown_folders = cluster->get_pending_folders();
    unknown_folders.put(u_folder);

    auto folder_info_my = folder_info_ptr_t();
    auto folder_info_peer = folder_info_ptr_t();
    {
        db::FolderInfo db_fi;
        db::set_index_id(db_fi, 5ul);
        db::set_max_sequence(db_fi, 10l);
        folder_info_my = folder_info_t::create(sequencer->next_uuid(), db_fi, my_device, folder).value();
    }
    {
        db::FolderInfo db_fi;
        db::set_index_id(db_fi, 6ul);
        db::set_max_sequence(db_fi, 0l);
        folder_info_peer = folder_info_t::create(sequencer->next_uuid(), db_fi, peer_device, folder).value();
    }
    folder->get_folder_infos().put(folder_info_my);
    folder->get_folder_infos().put(folder_info_peer);

    auto bi1 = proto::BlockInfo();
    proto::set_size(bi1, 5);
    proto::set_hash(bi1, utils::sha256_digest(as_bytes("12345")).value());
    auto b1 = block_info_t::create(bi1).assume_value();

    auto bi2 = proto::BlockInfo();
    proto::set_size(bi2, 5);
    proto::set_hash(bi2, utils::sha256_digest(as_bytes("67890")).value());
    auto b2 = block_info_t::create(bi2).assume_value();

    auto bi3 = proto::BlockInfo();
    proto::set_size(bi3, 5);
    proto::set_hash(bi3, utils::sha256_digest(as_bytes("qqqqqq")).value());
    auto b3 = block_info_t::create(bi3).assume_value();

    auto &blocks_map = cluster->get_blocks();
    blocks_map.put(b1);
    blocks_map.put(b2);
    blocks_map.put(b3);

    proto::FileInfo pr_fi_my = [&]() {
        auto f = proto::FileInfo();
        proto::set_name(f, "a/b.txt");
        proto::set_size(f, 5ul);
        proto::set_block_size(f, 5ul);
        proto::set_sequence(f, 1);
        auto &v = proto::get_version(f);
        proto::add_counters(v, proto::Counter(my_device->device_id().get_uint(), 0));
        return f;
    }();
    auto fi_my = file_info_t::create(sequencer->next_uuid(), pr_fi_my, folder_info_my).value();
    folder_info_my->add_strict(fi_my);

    proto::FileInfo pr_fi_peer1 = [&]() {
        auto f = proto::FileInfo();
        proto::set_name(f, "a/c.txt");
        proto::set_size(f, 5ul);
        proto::set_block_size(f, 5ul);
        proto::set_sequence(f, 1);
        auto &v = proto::get_version(f);
        proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 0));
        return f;
    }();
    auto fi_peer1 = file_info_t::create(sequencer->next_uuid(), pr_fi_peer1, folder_info_peer).value();
    REQUIRE(folder_info_peer->add_strict(fi_peer1));
    REQUIRE(folder_info_peer->get_file_infos().size() == 1);

    proto::FileInfo pr_fi_peer2 = [&]() {
        auto f = proto::FileInfo();
        proto::set_name(f, "a/d.txt");
        proto::set_size(f, 10ul);
        proto::set_block_size(f, 5ul);
        proto::set_sequence(f, 2);
        auto &v = proto::get_version(f);
        proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 0));
        return f;
    }();
    auto fi_peer2 = file_info_t::create(sequencer->next_uuid(), pr_fi_peer2, folder_info_peer).value();
    REQUIRE(folder_info_peer->add_strict(fi_peer2));
    REQUIRE(folder_info_peer->get_file_infos().size() == 2);

    fi_my->assign_block(b1, 0);
    fi_peer1->assign_block(b2, 0);
    fi_peer2->assign_block(b2, 0);
    fi_peer2->assign_block(b3, 1);

    auto cc = std::make_unique<proto::ClusterConfig>();
    auto &pr_folder = proto::add_folders(*cc);
    proto::set_id(pr_folder, folder->get_id());
    proto::set_label(pr_folder, folder->get_label());
    auto &pr_peer = proto::add_devices(pr_folder);
    proto::set_id(pr_peer, peer_id.get_sha256());
    proto::set_name(pr_peer, peer_device->get_name());
    proto::set_max_sequence(pr_peer, 123456u);
    proto::set_index_id(pr_peer, 7u);

    auto diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
    REQUIRE(diff_opt);

    auto &diff = diff_opt.value();
    REQUIRE(diff->apply(*cluster, get_apply_controller()));

    auto folder_info_peer_new = folder->get_folder_infos().by_device(*peer_device);
    REQUIRE(folder_info_peer_new);
    REQUIRE(folder_info_peer_new == folder_info_peer);
    REQUIRE(folder_info_peer_new->get_file_infos().size() == 0);
    CHECK(folder_info_peer_new->get_index() == 7ul);
    CHECK(folder_info_peer_new->get_max_sequence() == 0);

    CHECK(folder_info_peer->get_file_infos().size() == 0);
    CHECK(fi_peer1->use_count() == 1);
    CHECK(fi_peer2->use_count() == 1);

    CHECK(blocks_map.size() == 1);
    CHECK(blocks_map.by_hash(b1->get_hash()));

    bool visited = false;
    auto visitor = my_cluster_update_visitor_t([&](auto &) { visited = true; });
    auto r_v = diff->visit(visitor, nullptr);
    REQUIRE(r_v);
    REQUIRE(visited);
    CHECK(visitor.remove_blocks == 1);
    CHECK(visitor.remove_files == 1);
    CHECK(visitor.remove_folders == 0);
    CHECK(visitor.remove_pending_folders == 1);
    CHECK(visitor.updated_folders == 1);
}

TEST_CASE("cluster update for a folder, which was not shared", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    db::Folder db_folder;
    db::set_id(db_folder, "some-id");
    db::set_label(db_folder, "my-label");
    db::set_path(db_folder, "/my/path");
    auto folder = folder_t::create(sequencer->next_uuid(), db_folder).value();

    cluster->get_folders().put(folder);

    auto folder_info_my = folder_info_ptr_t();
    auto folder_info_peer = folder_info_ptr_t();
    {
        db::FolderInfo db_fi;
        db::set_index_id(db_fi, 5ul);
        db::set_max_sequence(db_fi, 10l);
        folder_info_my = folder_info_t::create(sequencer->next_uuid(), db_fi, my_device, folder).value();
        folder->get_folder_infos().put(folder_info_my);
    }

    auto cc = std::make_unique<proto::ClusterConfig>();
    auto &pr_folder = proto::add_folders(*cc);
    proto::set_id(pr_folder, folder->get_id());
    proto::set_label(pr_folder, folder->get_label());
    auto &pr_peer = proto::add_devices(pr_folder);
    proto::set_id(pr_peer, peer_id.get_sha256());
    proto::set_name(pr_peer, peer_device->get_name());
    proto::set_max_sequence(pr_peer, 123456u);
    proto::set_index_id(pr_peer, 7u);

    auto diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
    REQUIRE(diff_opt);
    auto &diff = diff_opt.value();
    REQUIRE(diff->apply(*cluster, get_apply_controller()));
}

TEST_CASE("cluster update with unknown devices", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id_1 =
        device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto peer_id_2 =
        device_id_t::from_string("EAMTZPW-Q4QYERN-D57DHFS-AUP2OMG-PAHOR3R-ZWLKGAA-WQC5SVW-UJ5NXQA").value();

    auto peer_device = device_t::create(peer_id_1, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    db::Folder db_folder;
    db::set_id(db_folder, "some-id");
    db::set_label(db_folder, "my-label");
    db::set_path(db_folder, "/my/path");
    auto folder = folder_t::create(sequencer->next_uuid(), db_folder).value();

    cluster->get_folders().put(folder);

    auto folder_info_my = folder_info_ptr_t();
    auto folder_info_peer = folder_info_ptr_t();
    {
        db::FolderInfo db_fi;
        db::set_index_id(db_fi, 5ul);
        db::set_max_sequence(db_fi, 10l);
        folder_info_my = folder_info_t::create(sequencer->next_uuid(), db_fi, my_device, folder).value();
        folder->get_folder_infos().put(folder_info_my);
    }
    {
        db::FolderInfo db_fi;
        db::set_index_id(db_fi, 6ul);
        db::set_max_sequence(db_fi, 0l);
        folder_info_peer = folder_info_t::create(sequencer->next_uuid(), db_fi, peer_device, folder).value();
        folder->get_folder_infos().put(folder_info_my);
    }
    folder->get_folder_infos().put(folder_info_my);
    folder->get_folder_infos().put(folder_info_peer);

    auto cc = std::make_unique<proto::ClusterConfig>();

    auto &pr_folder = proto::add_folders(*cc);
    proto::set_id(pr_folder, folder->get_id());
    proto::set_label(pr_folder, folder->get_label());

    auto &pr_peer_1 = proto::add_devices(pr_folder);
    proto::set_id(pr_peer_1, peer_id_1.get_sha256());
    proto::set_name(pr_peer_1, peer_device->get_name());
    proto::set_max_sequence(pr_peer_1, 123456u);
    proto::set_index_id(pr_peer_1, 7u);

    auto &pr_peer_2 = proto::add_devices(pr_folder);
    proto::set_id(pr_peer_2, peer_id_2.get_sha256());
    proto::set_name(pr_peer_2, peer_device->get_name());
    proto::set_max_sequence(pr_peer_2, 123456u);
    proto::set_index_id(pr_peer_2, 7u);

    auto diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
    REQUIRE(diff_opt);
}

TEST_CASE("cluster update nothing shared", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id_1 =
        device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto peer_id_2 =
        device_id_t::from_string("EAMTZPW-Q4QYERN-D57DHFS-AUP2OMG-PAHOR3R-ZWLKGAA-WQC5SVW-UJ5NXQA").value();

    auto peer_device = device_t::create(peer_id_1, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);
    auto &blocks_map = cluster->get_blocks();

    auto bi1 = proto::BlockInfo();
    proto::set_size(bi1, 5);
    proto::set_hash(bi1, utils::sha256_digest(as_bytes("12345")).value());
    auto b1 = block_info_t::create(bi1).assume_value();
    blocks_map.put(b1);

    db::Folder db_folder;
    db::set_id(db_folder, "some-id");
    db::set_label(db_folder, "my-label");
    db::set_path(db_folder, "/my/path");
    auto folder = folder_t::create(sequencer->next_uuid(), db_folder).value();

    cluster->get_folders().put(folder);

    auto folder_info_my = folder_info_ptr_t();
    auto folder_info_peer = folder_info_ptr_t();
    {
        db::FolderInfo db_fi;
        db::set_index_id(db_fi, 5ul);
        db::set_max_sequence(db_fi, 10l);
        folder_info_my = folder_info_t::create(sequencer->next_uuid(), db_fi, my_device, folder).value();
        folder->get_folder_infos().put(folder_info_my);
    }
    {
        db::FolderInfo db_fi;
        db::set_index_id(db_fi, 6ul);
        db::set_max_sequence(db_fi, 0l);
        folder_info_peer = folder_info_t::create(sequencer->next_uuid(), db_fi, peer_device, folder).value();
        folder->get_folder_infos().put(folder_info_peer);

        proto::FileInfo pr_fi_peer = [&]() {
            auto f = proto::FileInfo();
            proto::set_name(f, "a/c.txt");
            proto::set_size(f, 5ul);
            proto::set_block_size(f, 5ul);
            proto::set_sequence(f, 5);
            auto &v = proto::get_version(f);
            proto::add_counters(v, proto::Counter(peer_device->device_id().get_uint(), 0));
            return f;
        }();

        auto fi_peer = file_info_t::create(sequencer->next_uuid(), pr_fi_peer, folder_info_peer).value();
        fi_peer->assign_block(b1, 0);
        REQUIRE(folder_info_peer->add_strict(fi_peer));

        REQUIRE(folder_info_peer->get_file_infos().size() == 1);
    }

    auto cc = std::make_unique<proto::ClusterConfig>();
    auto diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
    REQUIRE(diff_opt);
    auto opt = diff_opt.value()->apply(*cluster, get_apply_controller());
    REQUIRE(opt);

    CHECK(blocks_map.size() == 0);
    CHECK(folder_info_peer->get_file_infos().size() == 0);
    CHECK(folder->is_shared_with(*peer_device));
    CHECK(folder->is_shared_with(*peer_device)->get_file_infos().size() == 0);
}

TEST_CASE("non-shared pending folder", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id_1 =
        device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id_1, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto builder = diff_builder_t(*cluster);
    auto folder_id = "1234";

    auto sha256 = peer_id_1.get_sha256();
    REQUIRE(builder.upsert_folder(folder_id, "/my/path").apply());
    REQUIRE(builder.configure_cluster(sha256).add(sha256, folder_id, 5, 4).finish().apply());
    REQUIRE(cluster->get_pending_folders().size() == 1);

    auto &uf = *cluster->get_pending_folders().begin()->item;
    REQUIRE(uf.device_id() == peer_id_1);
    REQUIRE(uf.get_index() == 5);
    REQUIRE(uf.get_max_sequence() == 4);
}

TEST_CASE("cluster update with remote folders (1)", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id_1 =
        device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id_1, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    db::Folder db_folder;
    db::set_id(db_folder, "some-id");
    db::set_label(db_folder, "my-label");
    db::set_path(db_folder, "/my/path");
    auto folder = folder_t::create(sequencer->next_uuid(), db_folder).value();

    cluster->get_folders().put(folder);

    auto folder_info_my = folder_info_ptr_t();
    auto folder_info_peer = folder_info_ptr_t();
    {
        db::FolderInfo db_fi;
        db::set_index_id(db_fi, 5ul);
        db::set_max_sequence(db_fi, 10l);
        folder_info_my = folder_info_t::create(sequencer->next_uuid(), db_fi, my_device, folder).value();
        folder->get_folder_infos().put(folder_info_my);
    }
    {
        db::FolderInfo db_fi;
        db::set_index_id(db_fi, 6ul);
        db::set_max_sequence(db_fi, 0l);
        folder_info_peer = folder_info_t::create(sequencer->next_uuid(), db_fi, peer_device, folder).value();
        folder->get_folder_infos().put(folder_info_my);
    }
    folder->get_folder_infos().put(folder_info_my);
    folder->get_folder_infos().put(folder_info_peer);

    auto cc = std::make_unique<proto::ClusterConfig>();

    auto &pr_folder = proto::add_folders(*cc);
    proto::set_id(pr_folder, folder->get_id());
    proto::set_label(pr_folder, folder->get_label());

    auto &pr_peer = proto::add_devices(pr_folder);
    proto::set_id(pr_peer, peer_id_1.get_sha256());
    proto::set_name(pr_peer, peer_device->get_name());
    proto::set_max_sequence(pr_peer, 123456u);
    proto::set_index_id(pr_peer, 7u);

    auto &pr_peer_my = proto::add_devices(pr_folder);
    proto::set_id(pr_peer_my, my_id.get_sha256());
    proto::set_name(pr_peer_my, my_device->get_name());
    proto::set_max_sequence(pr_peer_my, 3);
    proto::set_index_id(pr_peer_my, 5ul);

    auto diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
    REQUIRE(diff_opt);

    auto opt = diff_opt.value()->apply(*cluster, get_apply_controller());
    REQUIRE(opt);

    auto remote_folder = peer_device->get_remote_folder_infos().by_folder(*folder);
    REQUIRE(remote_folder);
    CHECK(remote_folder->get_index() == 5ul);
    CHECK(remote_folder->get_max_sequence() == 3);

    SECTION("unshare by peer") {
        auto cc = std::make_unique<proto::ClusterConfig>();
        diff_opt = diff::peer::cluster_update_t::create({}, *cluster, *sequencer, *peer_device, *cc);
        REQUIRE(diff_opt);

        auto &diff = diff_opt.value();
        REQUIRE(diff->apply(*cluster, get_apply_controller()));

        CHECK(peer_device->get_remote_folder_infos().size() == 0);
        auto fi = folder->get_folder_infos().by_device(*peer_device);
        // we are still sharing the folder with peer
        CHECK(fi);
    }
}

TEST_CASE("cluster update, inactual remote folders for non-shared folder", "[model]") {
    auto root_path = unique_path();
    auto path_guard = path_guard_t(root_path);

    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    auto &devices = cluster->get_devices();
    devices.put(my_device);
    devices.put(peer_device);

    auto builder = diff_builder_t(*cluster);
    auto folder_1_id = "1234";

    auto sha256 = peer_id.get_sha256();

    auto r = builder.configure_cluster(sha256, root_path)
                 .add(sha256, folder_1_id, 5, 4)
                 .add(my_id.get_sha256(), folder_1_id, 55, 44)
                 .finish()
                 .apply();
    REQUIRE(r);
    REQUIRE(cluster->get_folders().size() == 0);
}

TEST_CASE("device introduction", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id_1 =
        device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto peer_id_2 =
        device_id_t::from_string("EAMTZPW-Q4QYERN-D57DHFS-AUP2OMG-PAHOR3R-ZWLKGAA-WQC5SVW-UJ5NXQA").value();

    auto peer_device_1 = device_t::create(peer_id_1, "peer-device").value();
    auto db_peer_1 = db::Device();
    peer_device_1->serialize(db_peer_1);
    db::set_introducer(db_peer_1, true);
    REQUIRE(peer_device_1->update(db_peer_1));

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    auto &devices = cluster->get_devices();
    devices.put(my_device);
    devices.put(peer_device_1);

    auto builder = diff_builder_t(*cluster);
    auto folder_1_id = "1234";
    auto folder_2_id = "5678";
    auto folder_3_id = "abcd";

    auto sha256_1 = peer_id_1.get_sha256();
    auto sha256_2 = peer_id_2.get_sha256();

    auto r = builder.upsert_folder(folder_1_id, "/my/path-1")
                 .upsert_folder(folder_2_id, "/my/path-2")
                 .upsert_folder(folder_3_id, "/my/path-3")
                 .then()
                 .share_folder(sha256_1, folder_1_id, {})
                 .share_folder(sha256_1, folder_2_id, {})
                 .share_folder(sha256_1, folder_3_id, {})
                 .apply();
    REQUIRE(r);

    SECTION("non-emtpy introduced device address") {
        SECTION("dynamic address") {
            r = builder.configure_cluster(sha256_1)
                    .add(sha256_1, folder_1_id, 5, 4)
                    .add(sha256_2, folder_1_id, 55, 444, "dynamic")
                    .finish()
                    .apply();
            REQUIRE(r);
        }
        SECTION("invalid address") {
            auto ec = builder.configure_cluster(sha256_1)
                          .add(sha256_1, folder_1_id, 5, 4)
                          .add(sha256_2, folder_1_id, 55, 444, "some-invalid-url%%!")
                          .fail();
            REQUIRE(ec == utils::make_error_code(utils::error_code_t::malformed_url));
        }
    }

    SECTION("emtpy introduced device address") {
        r = builder.configure_cluster(sha256_1)
                .add(sha256_1, folder_1_id, 5, 4)
                .add(sha256_2, folder_1_id, 55, 444)
                .add(sha256_1, folder_2_id, 6, 4)
                .add(sha256_2, folder_2_id, 66, 444)
                .finish()
                .apply();
        REQUIRE(r);

        REQUIRE(devices.size() == 3);

        auto peer_device_2 = devices.by_sha256(peer_id_2.get_sha256());
        REQUIRE(peer_device_2);
        auto folder_1 = cluster->get_folders().by_id(folder_1_id);
        auto folder_2 = cluster->get_folders().by_id(folder_2_id);
        auto folder_3 = cluster->get_folders().by_id(folder_3_id);
        REQUIRE(folder_1->is_shared_with(*peer_device_2));
        REQUIRE(folder_2->is_shared_with(*peer_device_2));
        REQUIRE(!folder_3->is_shared_with(*peer_device_2));

        SECTION("introduce 3rd folder") {
            r = builder.configure_cluster(sha256_1)
                    .add(sha256_1, folder_1_id, 5, 4)
                    .add(sha256_2, folder_1_id, 55, 444)
                    .add(sha256_1, folder_2_id, 6, 4)
                    .add(sha256_2, folder_2_id, 66, 444)
                    .add(sha256_1, folder_3_id, 7, 4)
                    .add(sha256_2, folder_3_id, 77, 444)
                    .finish()
                    .apply();
            REQUIRE(r);
            CHECK(devices.size() == 3);
            CHECK(folder_1->is_shared_with(*peer_device_2));
            CHECK(folder_2->is_shared_with(*peer_device_2));
            CHECK(folder_3->is_shared_with(*peer_device_2));
        }
        SECTION("de-introduce 1 folder") {
            r = builder.configure_cluster(sha256_1)
                    .add(sha256_1, folder_1_id, 5, 4)
                    .add(sha256_2, folder_1_id, 55, 444)
                    .add(sha256_1, folder_2_id, 6, 4)
                    .finish()
                    .apply();
            REQUIRE(r);
            CHECK(devices.size() == 3);
            CHECK(folder_1->is_shared_with(*peer_device_2));
            CHECK(!folder_2->is_shared_with(*peer_device_2));
        }
        SECTION("de-introduce all folders => de-introduce device") {
            r = builder.configure_cluster(sha256_1)
                    .add(sha256_1, folder_1_id, 5, 4)
                    .add(sha256_1, folder_2_id, 6, 4)
                    .finish()
                    .apply();
            REQUIRE(r);
            CHECK(devices.size() == 2);
            CHECK(!folder_1->is_shared_with(*peer_device_2));
            CHECK(!folder_2->is_shared_with(*peer_device_2));
        }
        SECTION("don't de-introduce folders/devices if skip_introduction_removals") {
            db::set_skip_introduction_removals(db_peer_1, true);
            REQUIRE(peer_device_1->update(db_peer_1));

            r = builder.configure_cluster(sha256_1)
                    .add(sha256_1, folder_1_id, 5, 4)
                    .add(sha256_1, folder_2_id, 6, 4)
                    .finish()
                    .apply();
            REQUIRE(r);
            CHECK(devices.size() == 3);
            CHECK(folder_1->is_shared_with(*peer_device_2));
            CHECK(folder_2->is_shared_with(*peer_device_2));
        }
    }
}

TEST_CASE("auto-accept folders", "[model]") {
    auto root_path = unique_path();
    auto path_guard = path_guard_t(root_path);

    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id_1 =
        device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto peer_id_2 =
        device_id_t::from_string("EAMTZPW-Q4QYERN-D57DHFS-AUP2OMG-PAHOR3R-ZWLKGAA-WQC5SVW-UJ5NXQA").value();

    auto peer_device_1 = device_t::create(peer_id_1, "peer-device").value();
    auto db_peer_1 = db::Device();
    peer_device_1->serialize(db_peer_1);
    db::set_introducer(db_peer_1, true);
    db::set_auto_accept(db_peer_1, true);
    REQUIRE(peer_device_1->update(db_peer_1));

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    auto &devices = cluster->get_devices();
    devices.put(my_device);
    devices.put(peer_device_1);

    auto peer_device_2 = device_t::create(peer_id_2, "peer-device").value();

    auto builder = diff_builder_t(*cluster);
    auto folder_1_id = "1234";

    auto sha256_1 = peer_id_1.get_sha256();
    auto sha256_2 = peer_id_2.get_sha256();

    SECTION("folder already exists => just share it") {
        auto r = builder.upsert_folder(folder_1_id, "/my/path-1").apply();
        REQUIRE(r);

        r = builder.configure_cluster(sha256_1, root_path).add(sha256_1, folder_1_id, 5, 4).finish().apply();
        REQUIRE(r);

        REQUIRE(cluster->get_folders().size() == 1);
        auto folder_1 = cluster->get_folders().by_id(folder_1_id);
        REQUIRE(folder_1->is_shared_with(*peer_device_1));
    }
    SECTION("able to create dir by folder_id") {
        auto r = builder.configure_cluster(sha256_1, root_path).add(sha256_1, folder_1_id, 5, 4).finish().apply();
        REQUIRE(r);

        REQUIRE(cluster->get_folders().size() == 1);
        auto folder_1 = cluster->get_folders().by_id(folder_1_id);
        REQUIRE(folder_1->is_shared_with(*peer_device_1));
        CHECK(bfs::exists(root_path / folder_1_id));
    }
    SECTION("able to create dir by folder label") {
        auto r = builder.configure_cluster(sha256_1, root_path)
                     .add_named(sha256_1, folder_1_id, 5, 4, "zzz")
                     .finish()
                     .apply();
        REQUIRE(r);

        REQUIRE(cluster->get_folders().size() == 1);
        auto folder_1 = cluster->get_folders().by_id(folder_1_id);
        REQUIRE(folder_1->is_shared_with(*peer_device_1));
        CHECK(bfs::exists(root_path / "zzz"));
    }
#ifndef SYNCSPIRIT_WIN
    SECTION("not able to create dir by folder_id") {
        auto new_root = root_path / "sub-root";
        bfs::create_directories(new_root);
        bfs::permissions(new_root, bfs::perms::all, bfs::perm_options::remove);
        auto new_guard = path_guard_t(new_root);

        auto r = builder.configure_cluster(sha256_1, new_root).add(sha256_1, folder_1_id, 5, 4).fail();
        REQUIRE(r);

        REQUIRE(cluster->get_folders().size() == 0);
        REQUIRE(!cluster->get_folders().by_id(folder_1_id));

        bfs::permissions(new_root, bfs::perms::all, bfs::perm_options::add);
        CHECK(!bfs::exists(new_root / folder_1_id));
    }
#endif
    SECTION("auto accept + introduce peer (source first)") {
        auto r = builder.configure_cluster(sha256_1, root_path)
                     .add(sha256_1, folder_1_id, 5, 4)
                     .add(sha256_2, folder_1_id, 55, 44)
                     .finish()
                     .apply();
        REQUIRE(r);

        REQUIRE(cluster->get_folders().size() == 1);
        REQUIRE(cluster->get_devices().size() == 3);
        auto folder_1 = cluster->get_folders().by_id(folder_1_id);
        REQUIRE(folder_1->is_shared_with(*peer_device_1));
        REQUIRE(folder_1->is_shared_with(*peer_device_2));
        CHECK(bfs::exists(root_path / folder_1_id));
    }
    SECTION("auto accept + introduce peer (source second)") {
        auto r = builder.configure_cluster(sha256_1, root_path)
                     .add(sha256_2, folder_1_id, 55, 44)
                     .add(sha256_1, folder_1_id, 5, 4)
                     .finish()
                     .apply();
        REQUIRE(r);

        REQUIRE(cluster->get_folders().size() == 1);
        REQUIRE(cluster->get_devices().size() == 3);
        auto folder_1 = cluster->get_folders().by_id(folder_1_id);
        REQUIRE(folder_1->is_shared_with(*peer_device_1));
        REQUIRE(folder_1->is_shared_with(*peer_device_2));
        CHECK(bfs::exists(root_path / folder_1_id));
    }
#ifndef SYNCSPIRIT_WIN
    SECTION("not able to create dir (source second)") {
        auto new_root = root_path / "sub-root";
        bfs::create_directories(new_root);
        bfs::permissions(new_root, bfs::perms::all, bfs::perm_options::remove);
        auto new_guard = path_guard_t(new_root);

        auto r = builder.configure_cluster(sha256_1, new_root)
                     .add(sha256_2, folder_1_id, 55, 44)
                     .add(sha256_1, folder_1_id, 5, 4)
                     .fail();
        REQUIRE(r);

        REQUIRE(cluster->get_folders().size() == 0);
        REQUIRE(cluster->get_devices().size() == 2);
        REQUIRE(!cluster->get_folders().by_id(folder_1_id));

        bfs::permissions(new_root, bfs::perms::all, bfs::perm_options::add);
        CHECK(!bfs::exists(new_root / folder_1_id));
    }
#endif
}

int _init() {
    test::init_logging();
    return 1;
}

static int v = _init();

repo_name: basiliscos/syncspirit

license: mit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "diff-builder.h"
#include "model/cluster.h"
#include "model/misc/sequencer.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("cluster modifications from ui", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto &folders = cluster->get_folders();
    auto id = std::string("1234-5678");
    auto label = std::string("my-label");
    auto path = std::string("/my/path");
    auto builder = diff_builder_t(*cluster);

    SECTION("folder create & update") {
        REQUIRE(builder.upsert_folder(id, path, label).apply());
        auto folder = folders.by_id(id);
        REQUIRE(folder);
        CHECK(folder->get_id() == id);
        CHECK(folder->get_label() == label);
        CHECK(folder->get_path() == path);
        CHECK(folder->get_cluster() == cluster);

        auto fi = folder->get_folder_infos().by_device(*my_device);
        REQUIRE(fi);
        CHECK(fi->get_max_sequence() == 0);
        CHECK(fi->get_index() != 0);

        REQUIRE(builder.upsert_folder(id, path, "label-2").apply());
        CHECK(folder->get_label() == "label-2");
    }

    SECTION("share folder") {
        REQUIRE(builder.upsert_folder(id, path, label).apply());

        SECTION("w/o unknown folder") {
            REQUIRE(builder.share_folder(peer_id.get_sha256(), id).apply());
            auto folder = folders.by_id(id);
            REQUIRE(folder);
            auto fi_peer = folder->get_folder_infos().by_device(*peer_device);
            REQUIRE(fi_peer);
            CHECK(fi_peer->get_device() == peer_device);
            CHECK(fi_peer->get_max_sequence() == 0);
        }

        SECTION("with unknown folder, then unshare") {
            auto db_pf = db::PendingFolder();
            auto &db_folder = db::get_folder(db_pf);
            db::set_id(db_folder, id);
            db::set_label(db_folder, label);
            db::set_path(db_folder, path);

            auto &db_fi = db::get_folder_info(db_pf);
            db::set_max_sequence(db_fi, 2345);
            db::set_index_id(db_fi, 12);

            auto uf = pending_folder_t::create(sequencer->next_uuid(), db_pf, peer_device->device_id()).value();
            cluster->get_pending_folders().put(uf);

            REQUIRE(builder.share_folder(peer_id.get_sha256(), id).apply());

            auto folder = folders.by_id(id);
            REQUIRE(folder);
            REQUIRE(folder->is_shared_with(*peer_device));
            auto fi_peer = folder->get_folder_infos().by_device(*peer_device);
            auto fi_my = folder->get_folder_infos().by_device(*my_device);
            REQUIRE(fi_my);
            REQUIRE(fi_peer);

            CHECK(fi_peer->get_device() == peer_device);
            CHECK(fi_peer->get_max_sequence() == 0);
            CHECK(fi_peer->get_index() == db::get_index_id(db_fi));
            CHECK(cluster->get_pending_folders().size() == 0);

            auto pr_file_1 = proto::FileInfo();
            proto::set_name(pr_file_1, "a.txt");
            proto::set_sequence(pr_file_1, 1l);
            proto::set_size(pr_file_1, 10);
            auto &pr_version = proto::get_version(pr_file_1);
            proto::add_counters(pr_version, proto::Counter(peer_device->device_id().get_uint(), 0));

            auto &b1 = proto::add_blocks(pr_file_1);
            proto::set_size(b1, 5);
            proto::set_hash(b1, utils::sha256_digest(as_bytes("12345")).value());
            auto bi_1 = block_info_t::create(b1).value();

            auto &b2 = proto::add_blocks(pr_file_1);
            proto::set_size(b2, 5);
            proto::set_hash(b2, utils::sha256_digest(as_bytes("567890")).value());
            auto bi_2 = block_info_t::create(b2).value();

            auto &blocks = cluster->get_blocks();
            blocks.put(bi_1);
            blocks.put(bi_2);

            auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_file_1, fi_peer).value();
            fi_peer->add_strict(file_peer);
            file_peer->assign_block(bi_1, 0);
            file_peer->assign_block(bi_2, 1);

            auto file_my = file_info_t::create(sequencer->next_uuid(), pr_file_1, fi_my).value();
            fi_my->add_strict(file_my);
            file_my->assign_block(bi_2, 1);

            REQUIRE(builder.unshare_folder(*fi_peer).apply());
            REQUIRE(!folder->get_folder_infos().by_device(*peer_device));
            REQUIRE(cluster->get_blocks().size() == 1);
            REQUIRE(!folder->is_shared_with(*peer_device));
        }
    }

    SECTION("update peer") {
        REQUIRE(builder.update_peer(my_id, "myyy-devices", "cn2").apply());
        CHECK(my_device->get_name() == "myyy-devices");
        CHECK(my_device->get_cert_name() == "cn2");
    }
}

int _init() {
    test::init_logging();
    return 1;
}

static int v = _init();


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "diff-builder.h"
#include "model/cluster.h"
#include "model/diff/modify/lock_file.h"
#include "model/diff/local/file_availability.h"
#include "model/diff/contact/update_contact.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("peer state update", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    rotor::address_ptr_t addr;
    auto builder = diff_builder_t(*cluster);
    REQUIRE(peer_device->get_state() == model::device_state_t::offline);

    auto connection_id = std::string("tcp://1.1.1.1:1");
    REQUIRE(builder.update_state(*peer_device, addr, device_state_t::online, connection_id).apply());
    CHECK(peer_device->get_state() == model::device_state_t::online);

    REQUIRE(builder.update_state(*peer_device, addr, device_state_t::offline, connection_id).apply());
    CHECK(peer_device->get_state() == model::device_state_t::offline);
}

TEST_CASE("with file", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    cluster->get_devices().put(my_device);

    auto builder = diff_builder_t(*cluster);
    builder.upsert_folder("1234-5678", "some/path", "my-label");
    REQUIRE(builder.apply());

    auto folder = cluster->get_folders().by_id("1234-5678");
    auto folder_info = folder->get_folder_infos().by_device(*my_device);

    auto pr_file = []() -> proto::FileInfo {
        auto f = proto::FileInfo();
        proto::set_name(f, "a.txt");
        proto::set_type(f, proto::FileInfoType::SYMLINK);
        proto::set_symlink_target(f, "/some/where");
        proto::set_block_size(f, 5);
        proto::set_size(f, 5);
        return f;
    }();
    auto b1_hash = utils::sha256_digest(as_bytes("12345")).value();
    auto &b1 = proto::add_blocks(pr_file);
    proto::set_hash(b1, b1_hash);
    proto::set_offset(b1, 0);
    proto::set_size(b1, 5);

    REQUIRE(builder.local_update(folder->get_id(), pr_file).apply());
    auto file = folder_info->get_file_infos().by_name("a.txt");
    REQUIRE(file);

    auto v = file->get_version();
    REQUIRE(v->counters_size() == 1);
    REQUIRE(proto::get_id(v->get_counter(0)) == my_device->device_id().get_uint());

    SECTION("lock/unlock") {
        auto diff = diff::cluster_diff_ptr_t(new diff::modify::lock_file_t(*file, true));
        REQUIRE(diff->apply(*cluster, get_apply_controller()));
        auto name = proto::get_name(pr_file);
        auto file = folder_info->get_file_infos().by_name(name);
        REQUIRE(file->is_locked());

        diff = diff::cluster_diff_ptr_t(new diff::modify::lock_file_t(*file, false));
        REQUIRE(diff->apply(*cluster, get_apply_controller()));
        REQUIRE(!file->is_locked());
    }

    SECTION("file_availability") {
        auto block = cluster->get_blocks().by_hash(b1_hash);
        file->remove_blocks();
        file->assign_block(block, 0);
        REQUIRE(!file->is_locally_available());
        auto diff = diff::cluster_diff_ptr_t(new diff::local::file_availability_t(file));
        REQUIRE(diff->apply(*cluster, get_apply_controller()));
        REQUIRE(file->is_locally_available());
    }
}

TEST_CASE("update_contact_t", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto peer_device = device_t::create(peer_id, "peer-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    {
        auto url_1 = utils::parse("tcp://192.168.100.6:22000");
        auto url_2 = utils::parse("tcp://192.168.100.6:22001");
        auto url_3 = utils::parse("tcp://192.168.100.6:22001");
        auto update_peer = diff::cluster_diff_ptr_t(
            new diff::contact::update_contact_t(*cluster, peer_id, utils::uri_container_t{url_1, url_1, url_2, url_3}));
        REQUIRE(update_peer->apply(*cluster, get_apply_controller()));
        auto &uris = peer_device->get_uris();
        REQUIRE(uris.size() == 2u);
        CHECK(*uris[0] == *url_1);
        CHECK(*uris[1] == *url_2);
    };
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "model/cluster.h"
#include "diff-builder.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("new file diff", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_device = device_t::create(peer_id, "peer-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto builder = diff_builder_t(*cluster);
    builder.upsert_folder("1234-5678", "some/path", "my-label");
    REQUIRE(builder.apply());

    auto folder = cluster->get_folders().by_id("1234-5678");

    proto::FileInfo pr_file;
    proto::set_name(pr_file, "a.txt");

    SECTION("symlink, inc sequence, no blocks") {
        proto::set_type(pr_file, proto::FileInfoType::SYMLINK);
        proto::set_symlink_target(pr_file, "/some/where");
        REQUIRE(builder.local_update(folder->get_id(), pr_file).apply());

        auto folder_info = folder->get_folder_infos().by_device(*my_device);
        auto &files = folder_info->get_file_infos();
        auto file = files.by_name(proto::get_name(pr_file));
        REQUIRE(file);
        REQUIRE(file->get_name() == "a.txt");
        REQUIRE(file->get_link_target() == "/some/where");
        REQUIRE(file->is_link());
        REQUIRE(file->get_sequence() == 1);
        REQUIRE(folder_info->get_max_sequence() == 1);
        REQUIRE(file->get_version()->counters_size() == 1);
        REQUIRE(file->get_modified_by() == my_device->device_id().get_uint());
        auto &counter = file->get_version()->get_best();
        auto v1 = proto::get_value(counter);
        CHECK(v1 > 0);

        SECTION("peer update") {
            file->get_version()->update(*peer_device);
            auto p = file->get_version()->as_proto();
            REQUIRE(proto::get_counters_size(p) == 2);

            auto &c0 = proto::get_counters(p, 0);
            CHECK(proto::get_value(c0) == v1);
            CHECK(proto::get_id(c0) == my_device->device_id().get_uint());

            auto &c1 = proto::get_counters(p, 1);
            CHECK(proto::get_value(c1) > v1);
            CHECK(proto::get_id(c1) == peer_device->device_id().get_uint());
        }

        SECTION("update it") {
            proto::set_symlink_target(pr_file, "/new/location");
            REQUIRE(builder.local_update(folder->get_id(), pr_file).apply());
            REQUIRE(files.size() == 1);

            auto new_file = files.by_name(file->get_name());
            REQUIRE(new_file);
            CHECK(new_file.get() == file.get());
            CHECK(new_file->get_key() == file->get_key());
            REQUIRE(new_file->get_version()->counters_size() == 1);
            auto v2 = proto::get_value(new_file->get_version()->get_best());
            REQUIRE(v1 < v2);
        }
    }

    SECTION("file, no inc, new block") {
        proto::set_type(pr_file, proto::FileInfoType::FILE);
        proto::set_size(pr_file, 5ul);
        proto::set_block_size(pr_file, 5ul);

        auto hash = utils::sha256_digest(as_bytes("12345")).value();
        auto &pr_block = proto::add_blocks(pr_file);
        proto::set_weak_hash(pr_block, 12);
        proto::set_size(pr_block, 5);
        proto::set_hash(pr_block, hash);

        REQUIRE(builder.local_update(folder->get_id(), pr_file).apply());

        auto folder_info = folder->get_folder_infos().by_device(*my_device);
        auto file = folder_info->get_file_infos().by_name(proto::get_name(pr_file));
        REQUIRE(file);
        REQUIRE(file->get_size() == 5);
        REQUIRE(file->get_name() == "a.txt");
        REQUIRE(!file->is_link());
        REQUIRE(file->get_sequence() == 1);
        REQUIRE(folder_info->get_max_sequence() == 1);
        REQUIRE(file->get_blocks().size() == 1);
        REQUIRE(file->get_blocks()[0]->get_hash() == hash);
        REQUIRE(cluster->get_blocks().size() == 1);
        REQUIRE(cluster->get_blocks().by_hash(hash));
    }

    SECTION("identical blocks") {
        proto::set_type(pr_file, proto::FileInfoType::FILE);
        proto::set_size(pr_file, 5ul);
        proto::set_block_size(pr_file, 5ul);
        proto::set_sequence(pr_file, 1ul);

        auto hash = utils::sha256_digest(as_bytes("12345")).value();
        auto &pr_block = proto::add_blocks(pr_file);
        proto::set_weak_hash(pr_block, 12);
        proto::set_size(pr_block, 5);
        proto::set_hash(pr_block, hash);
        REQUIRE(builder.local_update(folder->get_id(), pr_file).apply());

        auto folder_info = folder->get_folder_infos().by_device(*my_device);
        auto file = folder_info->get_file_infos().by_name(proto::get_name(pr_file));
        REQUIRE(file->get_sequence() == 1);
        CHECK(file->is_locally_available());

        proto::set_sequence(pr_file, 2ul);
        REQUIRE(builder.local_update(folder->get_id(), pr_file).apply());
        file = folder_info->get_file_infos().by_name(proto::get_name(pr_file));
        REQUIRE(file->get_sequence() == 2);
        CHECK(file->is_locally_available());
    }

    SECTION("delete file with blocks") {
        proto::set_type(pr_file, proto::FileInfoType::FILE);
        proto::set_size(pr_file, 5ul);
        proto::set_block_size(pr_file, 5ul);
        proto::set_sequence(pr_file, 1ul);

        auto hash = utils::sha256_digest(as_bytes("12345")).value();
        auto &pr_block = proto::add_blocks(pr_file);
        proto::set_weak_hash(pr_block, 12);
        proto::set_size(pr_block, 5);
        proto::set_hash(pr_block, hash);

        auto folder_info = folder->get_folder_infos().by_device(*my_device);
        auto &files = folder_info->get_file_infos();
        auto &blocks = cluster->get_blocks();
        REQUIRE(builder.local_update(folder->get_id(), pr_file).apply());
        REQUIRE(files.size() == 1);
        REQUIRE(blocks.size() == 1);

        auto file = folder_info->get_file_infos().by_name(proto::get_name(pr_file));
        auto sequence = file->get_sequence();
        auto version = file->get_version();

        proto::FileInfo pr_updated;
        proto::set_name(pr_updated, "a.txt");
        proto::set_deleted(pr_updated, true);
        REQUIRE(builder.local_update(folder->get_id(), pr_updated).apply());
        REQUIRE(files.size() == 1);
        CHECK(blocks.size() == 0);

        file = file = folder_info->get_file_infos().by_name(proto::get_name(pr_file));
        CHECK(file->is_deleted());
        CHECK(file->get_blocks().size() == 0);
        CHECK(file->get_sequence() > sequence);
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Ivan Baidakou

#include "test-utils.h"
#include "model/cluster.h"
#include "model/misc/sequencer.h"

#include "diff-builder.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("folder update (Index)", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = model::make_sequencer(5);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto index_id = uint64_t{0x1234};

    auto max_seq = int64_t(10);
    auto builder = diff_builder_t(*cluster);
    auto sha256 = peer_id.get_sha256();
    REQUIRE(builder.upsert_folder("1234-5678", "some/path", "my-label").apply());
    REQUIRE(builder.share_folder(sha256, "1234-5678").apply());
    REQUIRE(builder.configure_cluster(sha256).add(sha256, "1234-5678", index_id, max_seq).finish().apply());

    proto::FileInfo pr_fi;
    proto::set_name(pr_fi, "a.txt");
    proto::set_block_size(pr_fi, 5ul);
    proto::set_size(pr_fi, 6);
    proto::set_sequence(pr_fi, 5);

    auto b1_hash = utils::sha256_digest(as_bytes("12345")).value();
    auto &b1 = proto::add_blocks(pr_fi);
    proto::set_hash(b1, as_bytes("12345"));
    proto::set_size(b1, 5);

    auto ec = builder.make_index(sha256, "1234-5678").add(pr_fi, peer_device).fail();
    REQUIRE(ec);
    CHECK(ec == make_error_code(error_code_t::mismatch_file_size));
};

TEST_CASE("update folder-2 (via Index)", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto &folders = cluster->get_folders();
    auto builder = diff_builder_t(*cluster);

    REQUIRE(builder.upsert_folder("1234-5678", "/my/path").upsert_folder("5555-4444", "/p2").apply());
    auto folder = folders.by_id("1234-5678");
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());

    auto pr_index = proto::Index();
    proto::set_folder(pr_index, "1234-5678");

    auto sha256 = peer_id.get_sha256();
    SECTION("successful case") {
        auto peer_folder_info = folder->get_folder_infos().by_device(*peer_device);

        auto pr_file = proto::FileInfo();
        proto::set_name(pr_file, "a.txt");
        proto::set_block_size(pr_file, 5ul);
        proto::set_size(pr_file, 5ul);
        proto::set_sequence(pr_file, 10ul);
        proto::set_modified_s(pr_file, 1);

        auto &b = proto::add_blocks(pr_file);
        proto::set_hash(b, as_bytes("123"));
        proto::set_size(b, 5);

        SECTION("invalid cases") {
            auto ec = builder.make_index(sha256, "1234-5678").add(pr_file, peer_device, false).fail();
            REQUIRE(ec);
            CHECK(ec == model::make_error_code(model::error_code_t::missing_version));
        }

        SECTION("valid cases") {
            REQUIRE(builder.make_index(sha256, "1234-5678").add(pr_file, peer_device).finish().apply());

            auto &peer_files = peer_folder_info->get_file_infos();
            REQUIRE(peer_files.size() == 1);
            auto f = peer_files.by_name("a.txt");
            REQUIRE(f);

            auto key = f->get_key();

            SECTION("when a file with existing name is added, key & instance are kept") {
                proto::set_sequence(pr_file, 11ul);
                proto::set_modified_s(pr_file, 2);
                REQUIRE(builder.make_index(sha256, "1234-5678").add(pr_file, peer_device).finish().apply());

                REQUIRE(peer_files.size() == 1);
                auto same_f = peer_files.by_name(proto::get_name(pr_file));
                CHECK(same_f.get() == f.get());
                CHECK(same_f->get_key() == f->get_key());
            }

            SECTION("file with new blocks is added, the preivous one is removed") {
                auto &blocks_map = cluster->get_blocks();
                REQUIRE(blocks_map.size() == 1);
                auto prev_block = blocks_map.begin()->item;
                proto::set_sequence(pr_file, 11ul);
                proto::set_modified_s(pr_file, 2);
                proto::set_hash(b, as_bytes("345"));
                REQUIRE(builder.make_index(sha256, "1234-5678").add(pr_file, peer_device).finish().apply());

                REQUIRE(peer_files.size() == 1);
                REQUIRE(blocks_map.size() == 1);
                auto new_block = blocks_map.begin()->item;
                CHECK(new_block != prev_block);
                CHECK(new_block->get_hash() != prev_block->get_hash());
            }
        }
    }
    SECTION("folder does not exists") {
        auto ec = builder.make_index(sha256, "1234-5678-xxx").fail();
        REQUIRE(ec);
        CHECK(ec == model::make_error_code(model::error_code_t::folder_does_not_exist));
    }

    SECTION("folder is not shared") {
        auto ec = builder.make_index(sha256, "5555-4444").fail();
        REQUIRE(ec);
        CHECK(ec == model::make_error_code(model::error_code_t::folder_is_not_shared));
    }

    SECTION("blocks are not expected") {
        auto pr_file = proto::FileInfo();
        proto::set_name(pr_file, "a.txt");
        proto::set_block_size(pr_file, 5ul);
        proto::set_size(pr_file, 5ul);
        proto::set_sequence(pr_file, 10ul);
        proto::set_modified_s(pr_file, 1);
        proto::set_deleted(pr_file, true);

        auto &b = proto::add_blocks(pr_file);
        proto::set_hash(b, as_bytes("123"));
        proto::set_size(b, 5);

        auto ec = builder.make_index(sha256, "1234-5678").add(pr_file, peer_device).fail();
        REQUIRE(ec);
        CHECK(ec == model::make_error_code(model::error_code_t::unexpected_blocks));
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "access.h"
#include "model/cluster.h"
#include "diff-builder.h"

#include "model/diff/modify/append_block.h"
#include "model/diff/modify/clone_block.h"
#include "model/diff/local/blocks_availability.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("various block diffs", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();

    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    cluster->get_devices().put(my_device);

    auto builder = diff_builder_t(*cluster);
    builder.upsert_folder("1234-5678", "some/path", "my-label");
    REQUIRE(builder.apply());

    auto folder = cluster->get_folders().by_id("1234-5678");
    auto folder_info = folder->get_folder_infos().by_device(*my_device);

    auto b1_hash = utils::sha256_digest(as_bytes("12345")).value();
    auto b2_hash = utils::sha256_digest(as_bytes("567890")).value();

    proto::FileInfo pr_file;
    proto::set_name(pr_file, "a.txt");
    proto::set_block_size(pr_file, 5ul);
    proto::set_size(pr_file, 10ul);

    auto &b1 = proto::add_blocks(pr_file);
    proto::set_offset(b1, 0);
    proto::set_size(b1, 5);
    proto::set_hash(b1, b1_hash);

    auto &b2 = proto::add_blocks(pr_file);
    proto::set_offset(b2, 5);
    proto::set_size(b2, 5);
    proto::set_hash(b2, b2_hash);

    REQUIRE(builder.local_update(folder->get_id(), pr_file).apply());

    auto file = folder_info->get_file_infos().by_name("a.txt");
    auto bi1 = cluster->get_blocks().by_hash(b1_hash);
    auto bi2 = cluster->get_blocks().by_hash(b2_hash);
    file->remove_blocks();
    file->assign_block(bi1, 0);
    file->assign_block(bi2, 1);
    REQUIRE(!file->is_locally_available());

    SECTION("append") {
        auto diff_raw = new model::diff::modify::append_block_t(*file, 0, as_owned_bytes("12345"));
        auto diff = diff::cluster_diff_ptr_t(diff_raw);
        diff->assign_sibling(diff_raw->ack().get());
        REQUIRE(builder.assign(diff.get()).apply());
        auto &blocks = file->get_blocks();

        auto lf1 = blocks[0]->local_file();
        REQUIRE(lf1);
        CHECK(lf1.block_index() == 0);
        CHECK(lf1.get_offset() == 0);
        CHECK(lf1.is_locally_available());
        CHECK(!file->is_locally_available());
    }

    SECTION("clone, from different file") {
        proto::FileInfo pr_source;
        proto::set_name(pr_source, "b.txt");
        proto::set_block_size(pr_source, 5ul);
        proto::set_size(pr_source, 5ul);

        auto &b1 = proto::add_blocks(pr_source);
        proto::set_offset(b1, 0);
        proto::set_size(b1, 5);
        proto::set_hash(b1, b2_hash);

        REQUIRE(builder.local_update(folder->get_id(), pr_source).apply());
        auto source = folder_info->get_file_infos().by_name("b.txt");
        auto b2 = source->get_blocks().at(0);
        b2->mark_local_available(source.get());

        auto fb = model::file_block_t(bi2.get(), file.get(), 1);
        auto diff_raw = new model::diff::modify::clone_block_t(fb);
        auto diff = diff::cluster_diff_ptr_t(diff_raw);
        diff->assign_sibling(diff_raw->ack().get());
        REQUIRE(builder.assign(diff.get()).apply());

        auto &blocks = file->get_blocks();
        auto lf1 = blocks[1]->local_file();
        REQUIRE(lf1);
        CHECK(lf1.block_index() == 1);
        CHECK(lf1.get_offset() == 5);
        CHECK(lf1.is_locally_available());
        CHECK(!file->is_locally_available());
    }

    SECTION("availability") {
        using blocks_map_t = diff::local::blocks_availability_t::valid_blocks_map_t;
        auto map = blocks_map_t(2);
        map[0] = map[1] = true;
        auto diff = diff::cluster_diff_ptr_t(new diff::local::blocks_availability_t(*file, map));
        REQUIRE(diff->apply(*cluster, get_apply_controller()));
        CHECK(file->is_locally_available());
    }
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Ivan Baidakou

#include "test-utils.h"
#include "diff-builder.h"
#include "model/cluster.h"
#include "model/misc/error_code.h"
#include "model/misc/file_iterator.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

using A = advance_action_t;

TEST_CASE("folder upsert", "[model]") {
    using names_t = std::vector<std::string>;
    struct file_meta_t {
        std::string_view name;
        std::int64_t size;
        std::int64_t modified;
    };
    file_meta_t file_metas[5] = {
        {"0.txt", 0, 123}, {"1/a.txt", 5, 5000}, {"1/c.txt", 15, 4000}, {"1/d.txt", 10, 4500}, {"1/e.txt", 20, 6000},
    };

    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto builder = diff_builder_t(*cluster);

    auto &folders = cluster->get_folders();
    REQUIRE(builder.upsert_folder("1234-5678", "/my/path").apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());
    auto folder = folders.by_id("1234-5678");
    auto folder_infos = &folder->get_folder_infos();
    REQUIRE(folder_infos->size() == 2u);

    auto index = builder.make_index(peer_id.get_sha256(), folder->get_id());
    std::int64_t sequence = 10;
    for (auto &meta : file_metas) {
        auto pr = proto::FileInfo();
        proto::set_name(pr, meta.name);
        proto::set_size(pr, meta.size);
        proto::set_modified_s(pr, meta.modified);

        if (meta.size) {
            auto bytes_view = utils::bytes_view_t((unsigned char *)&file_metas, meta.size);
            auto hash = utils::sha256_digest(bytes_view).value();
            auto block = proto::BlockInfo();
            proto::set_hash(block, hash);
            proto::set_size(block, meta.size);
            proto::add_blocks(pr, std::move(block));
        }

        proto::set_sequence(pr, sequence++);
        index.add(pr, peer_device);
    }
    REQUIRE(index.finish().apply());

    auto file_iterator = peer_device->create_iterator(*cluster);
    auto names = names_t();
    auto next = [&]() {
        auto [fi, action] = file_iterator->next();
        REQUIRE(fi);
        CHECK(action == A::remote_copy);
        names.emplace_back(std::string(fi->get_name()));
    };

    next();
    next();
    auto expected_1 = names_t{"0.txt", "1/a.txt"};
    CHECK(names == expected_1);

    auto db_folder = db::Folder();
    folder->serialize(db_folder);
    db::set_pull_order(db_folder, db::PullOrder::largest);
    REQUIRE(builder.upsert_folder(db_folder).apply());
    REQUIRE(cluster->get_folders().size() == 1);
    folder = cluster->get_folders().by_id(folder->get_id());

    REQUIRE(folder->is_shared_with(*peer_device));

    next();
    next();
    next();

    auto expected_2 = names_t{"0.txt", "1/a.txt", "1/e.txt", "1/c.txt", "1/d.txt"};
    CHECK(names == expected_2);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "test-utils.h"
#include "diff-builder.h"
#include "model/cluster.h"
#include "model/misc/sequencer.h"

using namespace syncspirit;
using namespace syncspirit::model;
using namespace syncspirit::proto;
using namespace syncspirit::test;

TEST_CASE("new file diff", "[model]") {
    auto my_id = device_id_t::from_string("KHQNO2S-5QSILRK-YX4JZZ4-7L77APM-QNVGZJT-EKU7IFI-PNEPBMY-4MXFMQD").value();
    auto my_device = device_t::create(my_id, "my-device").value();
    auto peer_id = device_id_t::from_string("VUV42CZ-IQD5A37-RPEBPM4-VVQK6E4-6WSKC7B-PVJQHHD-4PZD44V-ENC6WAZ").value();

    auto peer_device = device_t::create(peer_id, "peer-device").value();
    auto cluster = cluster_ptr_t(new cluster_t(my_device, 1));
    auto sequencer = make_sequencer(4);
    cluster->get_devices().put(my_device);
    cluster->get_devices().put(peer_device);

    auto builder = diff_builder_t(*cluster);
    REQUIRE(builder.upsert_folder("1234-5678", "/my/path").apply());
    REQUIRE(builder.share_folder(peer_id.get_sha256(), "1234-5678").apply());

    auto &blocks_map = cluster->get_blocks();
    auto folder = cluster->get_folders().by_id("1234-5678");
    auto folder_my = folder->get_folder_infos().by_device(*my_device);
    auto folder_peer = folder->get_folder_infos().by_device(*peer_device);

    auto pr_file = proto::FileInfo();
    proto::set_name(pr_file, "a.txt");
    proto::set_sequence(pr_file, folder_peer->get_max_sequence() + 1);
    auto &version = proto::get_version(pr_file);
    auto &counter = proto::add_counters(version);

    proto::set_value(counter, 1);
    proto::set_id(counter, peer_device->device_id().get_uint());

    SECTION("trivial cases") {
        SECTION("no file on my side, clone blockless file") {
            auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_file, folder_peer).value();
            REQUIRE(folder_peer->add_strict(file_peer));
            REQUIRE(builder.remote_copy(*file_peer).apply());
            auto file_my = folder_my->get_file_infos().by_name(proto::get_name(pr_file));
            REQUIRE(file_my);
            CHECK(file_my->is_locally_available());
            CHECK(file_my->get_sequence() == 1);
            CHECK(file_my->get_folder_info() == folder_my.get());
            CHECK(folder_my->get_max_sequence() == 1);
        }

        SECTION("with blocks") {
            proto::set_size(pr_file, 5ul);
            proto::set_block_size(pr_file, 5ul);

            auto &b = proto::add_blocks(pr_file);
            proto::set_hash(b, as_bytes("12345"));
            proto::set_size(b, 5);
            auto bi = model::block_info_t::create(b).value();
            blocks_map.put(bi);

            auto file_my = file_info_t::create(sequencer->next_uuid(), pr_file, folder_my).value();
            file_my->assign_block(bi, 0);
            file_my->mark_local_available(0);
            REQUIRE(folder_my->add_strict(file_my));

            SECTION("with same blocks as peer") {
                proto::set_modified_s(pr_file, 123);
                auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_file, folder_peer).value();
                REQUIRE(folder_peer->add_strict(file_peer));
                file_peer->assign_block(bi, 0);
                file_peer->mark_local_available(0);
                REQUIRE(builder.remote_copy(*file_peer).apply());

                file_my = folder_my->get_file_infos().by_name(proto::get_name(pr_file));
                REQUIRE(file_my);
                CHECK(file_my->is_locally_available());
                CHECK(file_my->get_sequence() == 2);
                CHECK(file_my->get_modified_s() == 123);
                CHECK(file_my->get_folder_info() == folder_my.get());
                CHECK(folder_my->get_max_sequence() == 2);
            }

            SECTION("emty peer file") {
                proto::set_size(pr_file, 0);
                proto::set_block_size(pr_file, 0);
                auto file_peer = file_info_t::create(sequencer->next_uuid(), pr_file, folder_peer).value();
                REQUIRE(folder_peer->add_strict(file_peer));
                REQUIRE(builder.remote_copy(*file_peer).apply());

                file_my = folder_my->get_file_infos().by_name(proto::get_name(pr_file));
                REQUIRE(file_my);
                CHECK(file_my->is_locally_available());
                CHECK(file_my->get_sequence() == 2);
                CHECK(file_my->get_folder_info() == folder_my.get());
                CHECK(file_my->get_blocks().size() == 0);
                CHECK(folder_my->get_max_sequence() == 2);
                CHECK(blocks_map.size() == 0);
            }
        }
    }
}