/*
 *  Copyright (C) 2004-2026 Savoir-faire Linux Inc.
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

#include "manager.h"
#include "jamidht/conversationrepository.h"
#include "jamidht/gitserver.h"
#include "jamidht/jamiaccount.h"
#include "../../test_runner.h"
#include "jami.h"
#include "base64.h"
#include "fileutils.h"
#include "account_const.h"
#include "common.h"

#include <git2.h>

#include <dhtnet/connectionmanager.h>

#include <cppunit/TestAssert.h>
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>

#include <condition_variable>
#include <string>
#include <fstream>
#include <streambuf>
#include <filesystem>

using namespace std::string_literals;
using namespace libjami::Account;

namespace jami {
namespace test {

class ConversationRepositoryTest : public CppUnit::TestFixture
{
public:
    ConversationRepositoryTest()
    {
        // Init daemon
        libjami::init(libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG));
        if (not Manager::instance().initialized)
            CPPUNIT_ASSERT(libjami::start("jami-sample.yml"));
    }
    ~ConversationRepositoryTest() { libjami::fini(); }
    static std::string name() { return "ConversationRepository"; }
    void setUp();
    void tearDown();

    std::string aliceId;
    std::string bobId;
    std::mutex mtx;
    std::unique_lock<std::mutex> lk {mtx};
    std::condition_variable cv;

private:
    void testCreateRepository();
    void testAddSomeMessages();
    void testLogMessages();
    void testMerge();
    void testFFMerge();
    void testDiff();
    void testMergeProfileWithConflict();
    void testValidSignatureForCommit();
    void testInvalidSignatureForCommit();
    void testMalformedCommit();
    void testMalformedCommitUri();
    void testInvalidJoin();
    void testInvalidFile();
    void testMergeWithInvalidFile();
    void testCloneFailureDoesNotWipeExistingConversation();
    std::string addCommit(git_repository* repo,
                          const std::shared_ptr<JamiAccount> account,
                          const std::string& branch,
                          const std::string& commit_msg);
    std::string addMergeCommit(git_repository* repo,
                               const std::shared_ptr<JamiAccount> account,
                               const std::string& wanted_ref);
    void addAll(git_repository* repo);
    bool merge_in_main(const std::shared_ptr<JamiAccount> account, git_repository* repo, const std::string& commit_ref);
    CPPUNIT_TEST_SUITE(ConversationRepositoryTest);
    CPPUNIT_TEST(testCreateRepository);          // Passes
    CPPUNIT_TEST(testAddSomeMessages);           // Passes
    CPPUNIT_TEST(testLogMessages);               // Passes
    CPPUNIT_TEST(testMerge);                     // Passes
    CPPUNIT_TEST(testFFMerge);                   // Passes
    CPPUNIT_TEST(testDiff);                      // Passes
    CPPUNIT_TEST(testMergeProfileWithConflict);  // Passes
    CPPUNIT_TEST(testValidSignatureForCommit);   // Passes
    CPPUNIT_TEST(testInvalidSignatureForCommit); // Passes
    CPPUNIT_TEST(testMalformedCommit);           // Passes
    CPPUNIT_TEST(testMalformedCommitUri);        // Passes
    CPPUNIT_TEST(testInvalidJoin);               // Passes
    CPPUNIT_TEST(testInvalidFile);               // Passes
    CPPUNIT_TEST(testMergeWithInvalidFile);      // Passes
    CPPUNIT_TEST(testCloneFailureDoesNotWipeExistingConversation);
    CPPUNIT_TEST_SUITE_END();
};

CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(ConversationRepositoryTest, ConversationRepositoryTest::name());

void
ConversationRepositoryTest::setUp()
{
    auto actors = load_actors_and_wait_for_announcement("actors/alice-bob.yml");
    aliceId = actors["alice"];
    bobId = actors["bob"];
}

void
ConversationRepositoryTest::tearDown()
{
    wait_for_removal_of({aliceId, bobId});
}

void
ConversationRepositoryTest::testCreateRepository()
{
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    auto aliceDeviceId = DeviceId(std::string(aliceAccount->currentDeviceId()));
    auto uri = aliceAccount->getUsername();

    auto repository = ConversationRepository::createConversation(aliceAccount);

    // Assert that repository exists
    CPPUNIT_ASSERT(repository != nullptr);
    auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / repository->id();
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Assert that first commit is signed by alice
    git_repository* repo;
    CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);

    // 1. Verify that last commit is correctly signed by alice
    git_oid commit_id;
    CPPUNIT_ASSERT(git_reference_name_to_id(&commit_id, repo, "HEAD") == 0);

    git_buf signature = {}, signed_data = {};
    git_commit_extract_signature(&signature, &signed_data, repo, &commit_id, "signature");
    auto pk = base64::decode(std::string(signature.ptr, signature.ptr + signature.size));
    auto data = std::vector<uint8_t>(signed_data.ptr, signed_data.ptr + signed_data.size);
    git_repository_free(repo);

    CPPUNIT_ASSERT(aliceAccount->identity().second->getPublicKey().checkSignature(data, pk));

    // 2. Check created files
    auto CRLsPath = repoPath / "CRLs" / aliceDeviceId.toString();
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    auto adminCrt = repoPath / "admins" / (uri + ".crt");
    CPPUNIT_ASSERT(std::filesystem::is_regular_file(adminCrt));

    auto crt = std::ifstream(adminCrt);
    std::string adminCrtStr((std::istreambuf_iterator<char>(crt)), std::istreambuf_iterator<char>());

    auto cert = aliceAccount->identity().second;
    auto deviceCert = cert->toString(false);
    auto parentCert = cert->issuer->toString(true);

    CPPUNIT_ASSERT(adminCrtStr == parentCert);

    auto deviceCrt = repoPath / "devices" / (aliceDeviceId.toString() + ".crt");
    CPPUNIT_ASSERT(std::filesystem::is_regular_file(deviceCrt));

    crt = std::ifstream(deviceCrt);
    std::string deviceCrtStr((std::istreambuf_iterator<char>(crt)), std::istreambuf_iterator<char>());

    CPPUNIT_ASSERT(deviceCrtStr == deviceCert);
}

void
ConversationRepositoryTest::testAddSomeMessages()
{
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    auto repository = ConversationRepository::createConversation(aliceAccount);

    auto id1 = repository->commitMessage(R"({"body":"Commit 1","type":"text/plain"})");
    auto id2 = repository->commitMessage(R"({"body":"Commit 2","type":"text/plain"})");
    auto id3 = repository->commitMessage(R"({"body":"Commit 3","type":"text/plain"})");

    auto messages = repository->log();
    CPPUNIT_ASSERT(messages.size() == 4 /* 3 + initial */);
    CPPUNIT_ASSERT(messages[0].id == id3);
    CPPUNIT_ASSERT(messages[0].parents.front() == id2);
    CPPUNIT_ASSERT(messages[0].commitMsg.toString() == R"({"body":"Commit 3","type":"text/plain"})");
    CPPUNIT_ASSERT(messages[0].author.name == messages[3].author.name);
    CPPUNIT_ASSERT(messages[0].author.email == messages[3].author.email);
    CPPUNIT_ASSERT(messages[1].id == id2);
    CPPUNIT_ASSERT(messages[1].parents.front() == id1);
    CPPUNIT_ASSERT(messages[1].commitMsg.toString() == R"({"body":"Commit 2","type":"text/plain"})");
    CPPUNIT_ASSERT(messages[1].author.name == messages[3].author.name);
    CPPUNIT_ASSERT(messages[1].author.email == messages[3].author.email);
    CPPUNIT_ASSERT(messages[2].id == id1);
    CPPUNIT_ASSERT(messages[2].commitMsg.toString() == R"({"body":"Commit 1","type":"text/plain"})");
    CPPUNIT_ASSERT(messages[2].author.name == messages[3].author.name);
    CPPUNIT_ASSERT(messages[2].author.email == messages[3].author.email);
    CPPUNIT_ASSERT(messages[2].parents.front() == repository->id());
    // Check sig
    CPPUNIT_ASSERT(aliceAccount->identity().second->getPublicKey().checkSignature(messages[0].signed_content,
                                                                                  messages[0].signature));
    CPPUNIT_ASSERT(aliceAccount->identity().second->getPublicKey().checkSignature(messages[1].signed_content,
                                                                                  messages[1].signature));
    CPPUNIT_ASSERT(aliceAccount->identity().second->getPublicKey().checkSignature(messages[2].signed_content,
                                                                                  messages[2].signature));
}

void
ConversationRepositoryTest::testLogMessages()
{
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    auto repository = ConversationRepository::createConversation(aliceAccount);

    auto id1 = repository->commitMessage(R"({"body":"Commit 1","type":"text/plain"})");
    auto id2 = repository->commitMessage(R"({"body":"Commit 2","type":"text/plain"})");
    auto id3 = repository->commitMessage(R"({"body":"Commit 3","type":"text/plain"})");

    LogOptions options;
    options.from = repository->id();
    options.nbOfCommits = 1;
    auto messages = repository->log(options);
    CPPUNIT_ASSERT(messages.size() == 1);
    CPPUNIT_ASSERT(messages[0].id == repository->id());
    options.from = id2;
    options.nbOfCommits = 2;
    messages = repository->log(options);
    CPPUNIT_ASSERT(messages.size() == 2);
    CPPUNIT_ASSERT(messages[0].id == id2);
    CPPUNIT_ASSERT(messages[1].id == id1);
    options.from = repository->id();
    options.nbOfCommits = 3;
    messages = repository->log(options);
    CPPUNIT_ASSERT(messages.size() == 1);
    CPPUNIT_ASSERT(messages[0].id == repository->id());
}

std::string
ConversationRepositoryTest::addCommit(git_repository* repo,
                                      const std::shared_ptr<JamiAccount> account,
                                      const std::string& branch,
                                      const std::string& commit_msg)
{
    auto deviceId = DeviceId(std::string(account->currentDeviceId()));
    auto name = account->getDisplayName();
    if (name.empty())
        name = deviceId.toString();

    git_signature* sig_ptr = nullptr;
    // Sign commit's buffer
    if (git_signature_new(&sig_ptr, name.c_str(), deviceId.to_c_str(), std::time(nullptr), 0) < 0) {
        JAMI_ERROR("Unable to create a commit signature.");
        return {};
    }
    GitSignature sig {sig_ptr};

    // Retrieve current HEAD
    git_oid commit_id;
    if (git_reference_name_to_id(&commit_id, repo, "HEAD") < 0) {
        JAMI_ERROR("Unable to get reference for HEAD");
        return {};
    }

    git_commit* head_ptr = nullptr;
    if (git_commit_lookup(&head_ptr, repo, &commit_id) < 0) {
        JAMI_ERROR("Unable to look up HEAD commit");
        return {};
    }
    GitCommit head_commit {head_ptr};

    // Retrieve current index
    git_index* index_ptr = nullptr;
    if (git_repository_index(&index_ptr, repo) < 0) {
        JAMI_ERROR("Unable to open repository index");
        return {};
    }
    GitIndex index {index_ptr};

    git_oid tree_id;
    if (git_index_write_tree(&tree_id, index.get()) < 0) {
        JAMI_ERROR("Unable to write initial tree from index");
        return {};
    }

    git_tree* tree_ptr = nullptr;
    if (git_tree_lookup(&tree_ptr, repo, &tree_id) < 0) {
        JAMI_ERROR("Unable to look up initial tree");
        return {};
    }
    GitTree tree = GitTree(tree_ptr);

    git_buf to_sign = {};
#if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \
    && (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
    git_commit* const head_ref[1] = {head_commit.get()};
#else
    const git_commit* head_ref[1] = {head_commit.get()};
#endif
    if (git_commit_create_buffer(
            &to_sign, repo, sig.get(), sig.get(), nullptr, commit_msg.c_str(), tree.get(), 1, &head_ref[0])
        < 0) {
        JAMI_ERROR("Unable to create commit buffer");
        return {};
    }

    // git commit -S
    auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
    auto signed_buf = account->identity().first->sign(to_sign_vec);
    std::string signed_str = base64::encode(signed_buf);
    if (git_commit_create_with_signature(&commit_id, repo, to_sign.ptr, signed_str.c_str(), "signature") < 0) {
        JAMI_ERROR("Unable to sign commit");
        return {};
    }

    auto commit_str = git_oid_tostr_s(&commit_id);
    if (commit_str) {
        JAMI_LOG("New commit added with id: {}", commit_str);
        // Move commit to main branch
        git_reference* ref_ptr = nullptr;
        std::string branch_name = "refs/heads/" + branch;
        if (git_reference_create(&ref_ptr, repo, branch_name.c_str(), &commit_id, true, nullptr) < 0) {
            JAMI_WARNING("Unable to move commit to main");
        }
        git_reference_free(ref_ptr);
    }
    return commit_str ? commit_str : "";
}

std::string
ConversationRepositoryTest::addMergeCommit(git_repository* repo,
                                           const std::shared_ptr<JamiAccount> account,
                                           const std::string& wanted_ref)
{
    auto deviceId = DeviceId(std::string(account->currentDeviceId()));
    auto name = account->getDisplayName();
    if (name.empty())
        name = deviceId.toString();

    git_signature* sig_ptr = nullptr;
    if (git_signature_new(&sig_ptr, name.c_str(), deviceId.to_c_str(), std::time(nullptr), 0) < 0) {
        return {};
    }
    GitSignature sig {sig_ptr};

    // Get current HEAD (parent 1)
    git_oid head_oid;
    if (git_reference_name_to_id(&head_oid, repo, "HEAD") < 0) {
        JAMI_ERROR("Unable to get HEAD commit");
        return "";
    }

    git_commit* head_ptr = nullptr;
    if (git_commit_lookup(&head_ptr, repo, &head_oid) < 0) {
        JAMI_ERROR("Unable to look up HEAD commit");
        return "";
    }

    GitCommit head_commit {head_ptr};

    // Get wanted commit (parent 2)
    git_oid wanted_oid;
    if (git_oid_fromstr(&wanted_oid, wanted_ref.c_str()) < 0) {
        JAMI_ERROR("Unable to get wanted commit");
        return "";
    }

    git_commit* wanted_ptr = nullptr;
    if (git_commit_lookup(&wanted_ptr, repo, &wanted_oid) < 0) {
        JAMI_ERROR("Unable to look up wanted commit");
        return "";
    }
    GitCommit wanted_commit {wanted_ptr};

    // Get current index and tree (same as addCommit)
    git_index* index_ptr = nullptr;
    if (git_repository_index(&index_ptr, repo) < 0) {
        JAMI_ERROR("Unable to get repository index");
        return {};
    }
    GitIndex index {index_ptr};

    git_oid tree_id;
    if (git_index_write_tree(&tree_id, index.get()) < 0) {
        JAMI_ERROR("Unable to write tree from index");
        return {};
    }
    git_tree* tree_ptr = nullptr;
    if (git_tree_lookup(&tree_ptr, repo, &tree_id) < 0) {
        JAMI_ERROR("Unable to look up tree");
        return {};
    }
    GitTree tree = GitTree(tree_ptr);

    // Create merge message
    auto commitMsg = fmt::format("Merge commit '{}'", wanted_ref);

    git_buf to_sign = {};
#if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \
    && (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
    git_commit* const parents[2] = {head_commit.get(), wanted_commit.get()};
#else
    const git_commit* parents[2] = {head_commit.get(), wanted_commit.get()};
#endif
    if (git_commit_create_buffer(
            &to_sign, repo, sig.get(), sig.get(), nullptr, commitMsg.c_str(), tree.get(), 2, &parents[0])
        < 0) {
        return {};
    }

    auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
    auto signed_buf = account->identity().first->sign(to_sign_vec);
    std::string signed_str = base64::encode(signed_buf);
    git_oid commit_id;
    if (git_commit_create_with_signature(&commit_id, repo, to_sign.ptr, signed_str.c_str(), "signature") < 0) {
        git_buf_dispose(&to_sign);
        return {};
    }
    git_buf_dispose(&to_sign);

    auto commit_str = git_oid_tostr_s(&commit_id);
    if (commit_str) {
        // Move to main branch
        git_reference* ref_ptr = nullptr;
        if (git_reference_create(&ref_ptr, repo, "refs/heads/main", &commit_id, true, nullptr) < 0) {
            JAMI_WARNING("Unable to move commit to main");
            return {};
        }
        git_reference_free(ref_ptr);
    }
    return commit_str ? commit_str : "";
}

void
ConversationRepositoryTest::addAll(git_repository* repo)
{
    // git add -A
    git_index* index_ptr = nullptr;
    if (git_repository_index(&index_ptr, repo) < 0)
        return;
    GitIndex index {index_ptr};
    git_strarray array = {nullptr, 0};
    git_index_add_all(index.get(), &array, 0, nullptr, nullptr);
    git_index_write(index.get());
    git_strarray_free(&array);
}

void
ConversationRepositoryTest::testMerge()
{
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    auto repository = ConversationRepository::createConversation(aliceAccount);

    // Assert that repository exists
    CPPUNIT_ASSERT(repository != nullptr);
    auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / repository->id();
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Assert that first commit is signed by alice
    git_repository* repo;
    CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);
    auto id1 = addCommit(repo, aliceAccount, "main", R"({"body":"Commit 1","type":"text/plain"})");

    git_reference* ref = nullptr;
    git_commit* commit = nullptr;
    git_oid commit_id;
    git_oid_fromstr(&commit_id, repository->id().c_str());
    git_commit_lookup(&commit, repo, &commit_id);
    git_branch_create(&ref, repo, "to_merge", commit, false);
    git_reference_free(ref);
    git_repository_set_head(repo, "refs/heads/to_merge");

    auto id2 = addCommit(repo, aliceAccount, "to_merge", R"({"body":"Commit 2","type":"text/plain"})");
    git_repository_free(repo);

    // This will create a merge commit
    repository->merge(id2);

    CPPUNIT_ASSERT(repository->log().size() == 4 /* Initial, commit 1, 2, merge */);
}

void
ConversationRepositoryTest::testFFMerge()
{
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    auto repository = ConversationRepository::createConversation(aliceAccount);

    // Assert that repository exists
    CPPUNIT_ASSERT(repository != nullptr);
    auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / repository->id();
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Assert that first commit is signed by alice
    git_repository* repo;
    CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);
    auto id1 = addCommit(repo, aliceAccount, "main", R"({"body":"Commit 1","type":"text/plain"})");

    git_reference* ref = nullptr;
    git_commit* commit = nullptr;
    git_oid commit_id;
    git_oid_fromstr(&commit_id, id1.c_str());
    git_commit_lookup(&commit, repo, &commit_id);
    git_branch_create(&ref, repo, "to_merge", commit, false);
    git_reference_free(ref);
    git_repository_set_head(repo, "refs/heads/to_merge");

    auto id2 = addCommit(repo, aliceAccount, "to_merge", R"({"body":"Commit 2","type":"text/plain"})");
    git_repository_free(repo);

    // This will use a fast forward merge
    repository->merge(id2);

    CPPUNIT_ASSERT(repository->log().size() == 3 /* Initial, commit 1, 2 */);
}

void
ConversationRepositoryTest::testDiff()
{
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    auto aliceDeviceId = DeviceId(std::string(aliceAccount->currentDeviceId()));
    auto uri = aliceAccount->getUsername();
    auto repository = ConversationRepository::createConversation(aliceAccount);

    auto id1 = repository->commitMessage("Commit 1");
    auto id2 = repository->commitMessage("Commit 2");
    auto id3 = repository->commitMessage("Commit 3");

    auto diff = repository->diffStats(id2, id1);
    CPPUNIT_ASSERT(ConversationRepository::changedFiles(diff).empty());
    diff = repository->diffStats(id1);
    auto changedFiles = ConversationRepository::changedFiles(diff);
    CPPUNIT_ASSERT(!changedFiles.empty());
    CPPUNIT_ASSERT(changedFiles[0] == "admins/" + uri + ".crt");
    CPPUNIT_ASSERT(changedFiles[1] == "devices/" + aliceDeviceId.toString() + ".crt");
}

void
ConversationRepositoryTest::testMergeProfileWithConflict()
{
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    auto repository = ConversationRepository::createConversation(aliceAccount);

    // Assert that repository exists
    CPPUNIT_ASSERT(repository != nullptr);
    auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / repository->id();
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Assert that first commit is signed by alice
    git_repository* repo;
    CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);

    auto profile = std::ofstream(repoPath / "profile.vcf");
    if (profile.is_open()) {
        profile << "TITLE: SWARM\n";
        profile << "SUBTITLE: Some description\n";
        profile << "AVATAR: BASE64\n";
        profile.close();
    }
    addAll(repo);
    auto id1 = addCommit(repo, aliceAccount, "main", R"({"type":"application/update-profile"})");
    profile = std::ofstream(repoPath / "profile.vcf");
    if (profile.is_open()) {
        profile << "TITLE: SWARM\n";
        profile << "SUBTITLE: New description\n";
        profile << "AVATAR: BASE64\n";
        profile.close();
    }
    addAll(repo);
    auto id2 = addCommit(repo, aliceAccount, "main", R"({"type":"application/update-profile"})");

    git_reference* ref = nullptr;
    git_commit* commit = nullptr;
    git_oid commit_id;
    git_oid_fromstr(&commit_id, id1.c_str());
    git_commit_lookup(&commit, repo, &commit_id);
    git_branch_create(&ref, repo, "to_merge", commit, false);
    git_reference_free(ref);
    git_repository_set_head(repo, "refs/heads/to_merge");

    profile = std::ofstream(repoPath / "profile.vcf");
    if (profile.is_open()) {
        profile << "TITLE: SWARM\n";
        profile << "SUBTITLE: Another description\n";
        profile << "AVATAR: BASE64\n";
        profile.close();
    }
    addAll(repo);
    auto id3 = addCommit(repo, aliceAccount, "to_merge", R"({"type":"application/update-profile"})");

    // This will create a merge commit
    repository->merge(id3);
    CPPUNIT_ASSERT(repository->log().size() == 5 /* Initial, add, modify 1, modify 2, merge */);
}

void
ConversationRepositoryTest::testValidSignatureForCommit()
{
    // Get Alice's account
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    // Create a repository associated with Alice's account
    auto repository = ConversationRepository::createConversation(aliceAccount);
    // Assert the repository exists
    CPPUNIT_ASSERT(repository != nullptr);
    // Get the path to the conversation repository
    auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / repository->id();
    // Assert that the repository path is a directory
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Assert that the git repo was opened successfully
    git_repository* repo;
    CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);

    // Get the initial commit
    git_oid commit_id;
    CPPUNIT_ASSERT(git_reference_name_to_id(&commit_id, repo, "HEAD") == 0);

    // Get the commit object
    git_commit* commit_ptr = nullptr;
    CPPUNIT_ASSERT(git_commit_lookup(&commit_ptr, repo, &commit_id) == 0);

    // Extract the author email from the commit
    const git_signature* author = git_commit_author(commit_ptr);
    std::string userDevice = author->email;

    // Extract the signature and signature data of commit_id
    git_buf extracted_sig = {}, extracted_sig_data = {};
    // Extract the signature block and signature content from the commit
    git_commit_extract_signature(&extracted_sig, &extracted_sig_data, repo, &commit_id, "signature");
    // Verify the signature of the commit
    CPPUNIT_ASSERT(
        repository->isValidUserAtCommit(userDevice, git_oid_tostr_s(&commit_id), extracted_sig, extracted_sig_data));

    // Free git objects
    git_commit_free(commit_ptr);
    git_repository_free(repo);
}

void
ConversationRepositoryTest::testInvalidSignatureForCommit()
{
    // Get Alice and Bob's accounts
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
    // Create a repository associated with Alice's account
    auto repository = ConversationRepository::createConversation(aliceAccount);
    // Assert that the repository exists
    CPPUNIT_ASSERT(repository != nullptr);
    // Get the path to the conversation repository
    auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / repository->id();
    // Assert that the repository path is a directory
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Assert that the git repo was opened successfully
    git_repository* repo;
    CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);

    // Make a malicious commit that disguises itself as being sent from Alice
    // Retrieve current index
    git_index* index_ptr = nullptr;
    CPPUNIT_ASSERT(git_repository_index(&index_ptr, repo) == 0);
    GitIndex index {index_ptr};

    // Write the index to a new tree
    git_oid tree_id;
    // Assert that the tree was written successfully
    CPPUNIT_ASSERT(git_index_write_tree(&tree_id, index.get()) == 0);

    // Lookup the newly created tree
    git_tree* tree_ptr = nullptr;
    CPPUNIT_ASSERT(git_tree_lookup(&tree_ptr, repo, &tree_id) == 0);
    GitTree tree = GitTree(tree_ptr);

    // Create a commit object
    git_oid commit_id;
    CPPUNIT_ASSERT(git_reference_name_to_id(&commit_id, repo, "HEAD") == 0);

    // Lookup the HEAD commit
    git_commit* head_ptr = nullptr;
    CPPUNIT_ASSERT(git_commit_lookup(&head_ptr, repo, &commit_id) == 0);
    GitCommit head_commit {head_ptr};

    // The last argument of git_commit_create_buffer is of type
    // 'const git_commit **' in all versions of libgit2 except 1.8.0,
    // 1.8.1 and 1.8.3, in which it is of type 'git_commit *const *'.
#if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \
    && (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
    git_commit* const head_ref[1] = {head_commit.get()};
#else
    const git_commit* head_ref[1] = {head_commit.get()};
#endif

    // Create the signature for the commit with the current time and using the
    // Alice's device ID (i.e. her public key for her device)
    git_signature* sig = nullptr;
    CPPUNIT_ASSERT(git_signature_now(&sig,
                                     aliceAccount->getDisplayName().c_str(),
                                     std::string(aliceAccount->currentDeviceId()).c_str())
                   == 0);

    // Create a signed commit object and place it in a buffer
    git_buf to_sign = {};
    CPPUNIT_ASSERT(git_commit_create_buffer(&to_sign,
                                            repo,
                                            sig,
                                            sig,
                                            nullptr,
                                            "{\"body\":\"malicious intent\",\"type\":\"text/plain\"}",
                                            tree.get(),
                                            1,
                                            &head_ref[0])
                   == 0);
    git_signature_free(sig);

    // git commit -S
    auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
    // Sign the commit using Bob's private key
    std::vector<unsigned char> signed_buf = bobAccount->identity().first->sign(to_sign_vec);
    std::string signed_str = base64::encode(signed_buf);
    CPPUNIT_ASSERT(git_commit_create_with_signature(&commit_id, repo, to_sign.ptr, signed_str.c_str(), "signature")
                   == 0);

    // Dispose the signature buffer
    git_buf_dispose(&to_sign);

    // Move commit to main branch
    git_reference* ref_ptr = nullptr;
    CPPUNIT_ASSERT((git_reference_create(&ref_ptr, repo, "refs/heads/main", &commit_id, true, nullptr) == 0));

    CPPUNIT_ASSERT(repo);

    // Lookup the commit
    git_commit* commit_ptr = nullptr;
    CPPUNIT_ASSERT(git_commit_lookup(&commit_ptr, repo, &commit_id) == 0);

    // Get the author and user device, and assert that the user is not valid at the commit
    const git_signature* author = git_commit_author(commit_ptr);
    std::string userDevice = author->email;

    // Extract the signature and signature data of commit_id
    git_buf extracted_sig = {}, extracted_sig_data = {};
    // Extract the signature block and signature content from the commit
    git_commit_extract_signature(&extracted_sig, &extracted_sig_data, repo, &commit_id, "signature");
    CPPUNIT_ASSERT(
        !repository->isValidUserAtCommit(userDevice, git_oid_tostr_s(&commit_id), extracted_sig, extracted_sig_data));

    // Free git objects
    git_reference_free(ref_ptr);
    git_repository_free(repo);
    git_buf_dispose(&extracted_sig);
    git_buf_dispose(&extracted_sig_data);
}

void
ConversationRepositoryTest::testMalformedCommit()
{
    // Register signals
    std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;

    // Variable to be set to true on detection of malformed commit
    bool isMalformed = false;

    // Signals for malformed commits in validCommits function
    confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>(
        [&](const std::string& /*accountId*/,
            const std::string& /*conversationId*/,
            int code,
            const std::string& /*malformedSpec*/) {
            if (code == EVALIDFETCH) {
                isMalformed = true;
            }
            cv.notify_one();
        }));
    libjami::registerSignalHandlers(confHandlers);

    // Get Alice's account
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    // Create a repository associated with Alice's account
    auto repository = ConversationRepository::createConversation(aliceAccount);
    // Assert that repository exists
    CPPUNIT_ASSERT(repository != nullptr);
    // Get the path to the conversation repository
    auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / repository->id();
    // Assert that the repository path is a directory
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Assert that the git repo was opened successfully
    git_repository* repo;
    CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);

    // Create a malformed join commit
    auto malformedJoinID = addCommit(repo,
                                     aliceAccount,
                                     "main",
                                     "{\"action\":\"join\",\"type\":\"member\",\"uri\":"
                                     "\"joinGarbage\"}");

    // Log the repository
    auto conversationCommits = repository->log();
    // Call the validation for all the commits
    bool allCommitsValidated = repository->validCommits(conversationCommits);
    // Check that the appropriate signal was called
    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return isMalformed; }));
    // Check that the return value was false (i.e. malformed commit detected)
    CPPUNIT_ASSERT(!allCommitsValidated);
}

void
ConversationRepositoryTest::testMalformedCommitUri()
{
    // Register signals
    std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;

    // Variable to be set to true on detection of malformed commit
    bool isMalformed = false;

    // Signals for malformed commits in validCommits function
    confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>(
        [&](const std::string& /*accountId*/,
            const std::string& /*conversationId*/,
            int code,
            const std::string& /*malformedSpec*/) {
            if (code == EVALIDFETCH) {
                isMalformed = true;
            }
            cv.notify_one();
        }));
    libjami::registerSignalHandlers(confHandlers);

    // Get Alice's account
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    // Create a repository associated with Alice's account
    auto repository = ConversationRepository::createConversation(aliceAccount);
    // Assert that repository exists
    CPPUNIT_ASSERT(repository != nullptr);
    // Get the path to the conversation repository
    auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / repository->id();
    // Assert that the repository path is a directory
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Assert that the git repo was opened successfully
    git_repository* repo;
    CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);

    // Create malformed add and join commits with valid user InfoHash,
    // but with invalid "jami:" prefix
    std::string malformedID = "jami:" + aliceAccount->getUsername();
    std::string invitedPath = "invited/" + malformedID;

    CPPUNIT_ASSERT(!std::filesystem::exists(repoPath / invitedPath));

    // Simulate an invitation by creating the "invited/" directory with an empty file
    std::filesystem::create_directories(repoPath / "invited");
    std::ofstream(repoPath / invitedPath).close();
    CPPUNIT_ASSERT(std::filesystem::exists(repoPath / invitedPath));

    addAll(repo);

    auto malformedAddUserId = addCommit(repo,
                                        aliceAccount,
                                        "main",
                                        "{\"action\":\"add\",\"type\":\"member\",\"uri\":\"" + malformedID + "\"}");
    CPPUNIT_ASSERT(!malformedAddUserId.empty());

    auto malformedJoinUserID = addCommit(repo,
                                         aliceAccount,
                                         "main",
                                         "{\"action\":\"join\",\"type\":\"member\",\"uri\":\"" + malformedID + "\"}");
    CPPUNIT_ASSERT(!malformedJoinUserID.empty());

    // Log the repository
    auto conversationCommits = repository->log();
    // Call the validation for all the commits
    bool allCommitsValidated = repository->validCommits(conversationCommits);

    // Check that the appropriate signal was called
    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return isMalformed; }));
    // Check that the return value was false (i.e. malformed commit detected)
    CPPUNIT_ASSERT(!allCommitsValidated);
}

// A join without a previous add should be invalid
void
ConversationRepositoryTest::testInvalidJoin()
{
    // Register signals
    std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;

    // Variable to be set to true on detection of malformed commit
    bool isInvalid = false;

    // Signals for malformed commits in validCommits function
    confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>(
        [&](const std::string& /*accountId*/,
            const std::string& /*conversationId*/,
            int code,
            const std::string& /*malformedSpec*/) {
            if (code == EVALIDFETCH) {
                isInvalid = true;
            }
            cv.notify_one();
        }));
    libjami::registerSignalHandlers(confHandlers);

    // Get Alice's account
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    // Create a repository associated with Alice's account
    auto repository = ConversationRepository::createConversation(aliceAccount);
    // Assert that the repository exists
    CPPUNIT_ASSERT(repository != nullptr);
    // Get the path to the conversation repository
    auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / repository->id();
    // Assert that the repository path is a directory
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Assert that the git repo was opened successfully
    git_repository* repo;
    CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);

    std::string userId = aliceAccount->getUsername();

    auto invalidJoinUserID = addCommit(repo,
                                       aliceAccount,
                                       "main",
                                       "{\"action\":\"join\",\"type\":\"member\",\"uri\":\"" + userId + "\"}");
    CPPUNIT_ASSERT(!invalidJoinUserID.empty());

    // Log the repository
    auto conversationCommits = repository->log();
    // Call the validation for all the commits
    bool allCommitsValidated = repository->validCommits(conversationCommits);

    // Check that the appropriate signal was called
    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return isInvalid; }));
    // Check that the return value was false (i.e. invalid commit detected)
    CPPUNIT_ASSERT(!allCommitsValidated);
}

void
ConversationRepositoryTest::testInvalidFile()
{
    // Register signals
    std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;

    // Variable to be set to true on detection of malformed commit
    bool isInvalid = false;

    // Signals for malformed commits in validCommits function
    confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>(
        [&](const std::string& /*accountId*/,
            const std::string& /*conversationId*/,
            int code,
            const std::string& /*malformedSpec*/) {
            if (code == EVALIDFETCH) {
                isInvalid = true;
            }
            cv.notify_one();
        }));
    libjami::registerSignalHandlers(confHandlers);

    // Get Alice's account
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    // Create a repository associated with Alice's account
    auto repository = ConversationRepository::createConversation(aliceAccount);
    // Assert that repository exists
    CPPUNIT_ASSERT(repository != nullptr);
    // Get the path to the conversation repository
    auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / repository->id();
    // Assert that the repository path is a directory
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Assert that the git repo was opened successfully
    git_repository* repo;
    CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0);

    std::string userId = aliceAccount->getUsername();

    auto invalidFile = repoPath / "invalidFile.txt";
    std::ofstream(invalidFile).close();
    addAll(repo);

    auto invalidFileCommitID = addCommit(repo,
                                         aliceAccount,
                                         "main",
                                         "{\"type\":\"other\",\"note\":\"Add invalid file\"}");

    // Log the repository
    auto conversationCommits = repository->log();
    // Call the validation for all the commits
    bool allCommitsValidated = repository->validCommits(conversationCommits);

    // Check that the appropriate signal was called
    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return isInvalid; }));
    // Check that the return value was false (i.e. invalid commit detected)
    CPPUNIT_ASSERT(!allCommitsValidated);
}

void
ConversationRepositoryTest::testMergeWithInvalidFile()
{
    // Register signals
    std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;

    // Variable to be set to true on detection of malformed commit
    bool isInvalid = false;

    // Get member accounts
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
    auto aliceUri = aliceAccount->getUsername();
    auto bobUri = bobAccount->getUsername();

    auto bobRequestReceived = false;

    std::vector<libjami::SwarmMessage> aliceMessages = {};

    confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>(
        [&](const std::string& /*accountId*/,
            const std::string& /*conversationId*/,
            int code,
            const std::string& /*malformedSpec*/) {
            if (code == EVALIDFETCH) {
                isInvalid = true;
            }
            cv.notify_one();
        }));
    confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationRequestReceived>(
        [&](const std::string& accountId,
            const std::string& /* conversationId */,
            std::map<std::string, std::string> /*metadatas*/) {
            if (accountId == bobId) {
                bobRequestReceived = true;
            }
            cv.notify_one();
        }));
    confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::SwarmMessageReceived>(
        [&](const std::string& accountId, const std::string& /* conversationId */, libjami::SwarmMessage message) {
            if (accountId == aliceId) {
                aliceMessages.emplace_back(message);
            }
            cv.notify_one();
        }));

    libjami::registerSignalHandlers(confHandlers);

    auto convId = libjami::startConversation(aliceId);

    // Alice adds Bob to the conversation
    libjami::addConversationMember(aliceId, convId, bobUri);
    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobRequestReceived; }));

    auto aliceMsgSize = aliceMessages.size();
    libjami::acceptConversationRequest(bobId, convId);
    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return aliceMsgSize + 1 == aliceMessages.size(); }));

    auto convMembers = libjami::getConversationMembers(aliceId, convId);
    CPPUNIT_ASSERT(convMembers.size() == 2);

    // Alice and Bob exchange messages
    std::string msgId1 = "", msgId2 = "";
    aliceAccount->convModule()->sendMessage(convId, "1"s, "", true, {}, [&](bool, std::string commitId) {
        msgId1 = commitId;
        cv.notify_one();
    });
    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return !msgId1.empty(); }));
    bobAccount->convModule()->sendMessage(convId, "2"s, "", true, {}, [&](bool, std::string commitId) {
        msgId2 = commitId;
        cv.notify_one();
    });
    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return !msgId2.empty(); }));

    // Retrieve the repository associated with Alice's account
    auto aliceRepository = std::make_unique<ConversationRepository>(aliceAccount, convId);
    // Assert that the repository exists
    CPPUNIT_ASSERT(aliceRepository != nullptr);
    // Get the path to the conversation repository
    auto aliceRepoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations"
                         / aliceRepository->id();
    // Assert that the repository path is a directory
    CPPUNIT_ASSERT(std::filesystem::is_directory(aliceRepoPath));

    // Assert that Alice's git repo was opened successfully
    git_repository* aliceRepo;
    CPPUNIT_ASSERT(git_repository_open(&aliceRepo, aliceRepoPath.c_str()) == 0);

    // Bob adds an unwanted file
    auto bobRepository = std::make_unique<ConversationRepository>(bobAccount, convId);

    auto bobRepoPath = fileutils::get_data_dir() / bobAccount->getAccountID() / "conversations" / bobRepository->id();
    CPPUNIT_ASSERT(std::filesystem::is_directory(bobRepoPath));

    git_repository* bobRepo;
    CPPUNIT_ASSERT(git_repository_open(&bobRepo, bobRepoPath.c_str()) == 0);

    auto invalidBobFile = bobRepoPath / "invalidBobFile.txt";
    std::ofstream(invalidBobFile).close();
    addAll(bobRepo);

    // Add a fake merge commit associated with the invalid file
    auto invalidBobMergeCommitId = addMergeCommit(bobRepo, bobAccount, msgId2);

    // Log Bob's repository
    auto bobCommits = bobRepository->log();

    bool allCommitsValidated = bobRepository->validCommits(bobCommits);

    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return isInvalid; }));
    CPPUNIT_ASSERT(!allCommitsValidated);
}

void
ConversationRepositoryTest::testCloneFailureDoesNotWipeExistingConversation()
{
    // Regression test: a failed clone must NOT wipe an already-present
    // conversation directory at the destination. Previously
    // ConversationRepository::cloneConversation() unconditionally removed
    // the target directory before attempting the clone, which meant any
    // clone failure (network error, oversized pack, invalid remote, failed
    // validation, ...) resulted in permanent loss of the previously-valid
    // local conversation - opening the door to later syncing an empty
    // conversation on top of peers.
    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);

    // Create a real, valid conversation locally.
    auto repository = ConversationRepository::createConversation(aliceAccount);
    CPPUNIT_ASSERT(repository != nullptr);
    const auto convId = repository->id();
    const auto repoPath = fileutils::get_data_dir() / aliceAccount->getAccountID() / "conversations" / convId;
    CPPUNIT_ASSERT(std::filesystem::is_directory(repoPath));

    // Record the head so we can verify the original history survives the
    // failed clone attempt.
    const auto originalHead = repository->getHead();
    CPPUNIT_ASSERT(!originalHead.empty());

    // Drop a sentinel file inside the repo workdir; it must still be there
    // after the failed clone.
    const auto sentinel = repoPath / "sentinel.precious";
    {
        std::ofstream(sentinel) << "do-not-delete";
    }
    CPPUNIT_ASSERT(std::filesystem::is_regular_file(sentinel));

    // Release the repository handle so libgit2 isn't holding the workdir
    // open while we trigger the clone path.
    repository.reset();

    // Force clone to resolve to a local file:// URL that does not exist so
    // git_clone() fails fast without needing any network or peer setup.
    const bool savedFlag = ConversationRepository::FETCH_FROM_LOCAL_REPOS;
    ConversationRepository::FETCH_FROM_LOCAL_REPOS = true;

    auto [clonedRepo, commits] = ConversationRepository::cloneConversation(aliceAccount,
                                                                           "device-that-does-not-exist",
                                                                           convId);

    ConversationRepository::FETCH_FROM_LOCAL_REPOS = savedFlag;

    // Clone MUST have failed - the source does not exist.
    CPPUNIT_ASSERT(clonedRepo == nullptr);
    CPPUNIT_ASSERT(commits.empty());

    // The pre-existing conversation directory, the sentinel file, and the
    // original git history MUST still be intact. This is the behaviour the
    // fix guarantees; without it, the directory and its contents are gone.
    CPPUNIT_ASSERT_MESSAGE("Pre-existing conversation directory was wiped by a failed clone",
                           std::filesystem::is_directory(repoPath));
    CPPUNIT_ASSERT_MESSAGE("Sentinel file inside the pre-existing conversation was wiped",
                           std::filesystem::is_regular_file(sentinel));

    // Re-open the repository and confirm the git head is unchanged.
    auto reopened = std::make_unique<ConversationRepository>(aliceAccount, convId);
    CPPUNIT_ASSERT(reopened != nullptr);
    CPPUNIT_ASSERT_EQUAL(originalHead, reopened->getHead());
}

} // namespace test
} // namespace jami

CORE_TEST_RUNNER(jami::test::ConversationRepositoryTest::name())
