BareGit
// Unit tests for the inventory library. Run only when
// -DOVERSEER_BUILD_TEST=ON.
#include <array>
#include <cstdint>
#include <memory>
#include <span>
#include <string>
#include <vector>

#include <gtest/gtest.h>

#include <mw/database.hpp>
#include <mw/error.hpp>

#include "db/migrations.hpp"
#include "inventory/repo.hpp"
#include "support/seed.hpp"

namespace inv = overseer::inventory;

namespace
{

std::unique_ptr<mw::SQLite> makeMemDB()
{
    auto db = mw::SQLite::connectMemory();
    EXPECT_TRUE(db.has_value());
    auto& d = **db;
    EXPECT_TRUE(overseer::db::migrateDB0To1(d).has_value());
    return std::move(*db);
}

std::string errMsg(const mw::Error& e)
{
    return mw::errorMsg(e);
}

} // namespace

// ---- storage ---------------------------------------------------------

TEST(InventoryRepo, CreateAndGetStorage)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage s;
    s.name = "Living Room";
    auto id_r = repo.createStorage(s);
    ASSERT_TRUE(id_r.has_value());
    auto got_r = repo.getStorage(*id_r);
    ASSERT_TRUE(got_r.has_value());
    EXPECT_EQ(got_r->name, "Living Room");
    EXPECT_FALSE(got_r->parent_id.has_value());
}

TEST(InventoryRepo, CreateStorageEmptyNameIsBadInput)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto r = repo.createStorage(inv::Storage{});
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_BAD_INPUT);
}

TEST(InventoryRepo, GetUnknownStorageReturnsNotFound)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto r = repo.getStorage(9999);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NOT_FOUND);
}

TEST(InventoryRepo, SiblingNameCollisionIsTaken)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage parent;
    parent.name = "Apartment";
    auto pid = *repo.createStorage(parent);

    inv::Storage a, dup;
    a.name = "Room"; a.parent_id = pid;
    dup.name = "Room"; dup.parent_id = pid;
    ASSERT_TRUE(repo.createStorage(a).has_value());
    auto r = repo.createStorage(dup);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NAME_TAKEN);
}

TEST(InventoryRepo, RootNameCollisionIsTaken)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage a, dup;
    a.name = "Root"; dup.name = "Root";
    ASSERT_TRUE(repo.createStorage(a).has_value());
    auto r = repo.createStorage(dup);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NAME_TAKEN);
}

TEST(InventoryRepo, ListChildrenRootsAndNested)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto seeded = overseer::test::seedFamily(repo);

    auto roots = *repo.listChildren(std::nullopt);
    ASSERT_EQ(roots.size(), 1U);
    EXPECT_EQ(roots[0].id, seeded.apartment_id);

    auto kids = *repo.listChildren(seeded.apartment_id);
    ASSERT_EQ(kids.size(), 1U);
    EXPECT_EQ(kids[0].id, seeded.room_id);
}

TEST(InventoryRepo, PathToBuildsRootFirst)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto seeded = overseer::test::seedFamily(repo);

    auto path = *repo.pathTo(seeded.shelf_id);
    ASSERT_EQ(path.size(), 3U);
    EXPECT_EQ(path[0].id, seeded.apartment_id);
    EXPECT_EQ(path[1].id, seeded.room_id);
    EXPECT_EQ(path[2].id, seeded.shelf_id);
}

TEST(InventoryRepo, PathToUnknownIsNotFound)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto r = repo.pathTo(7777);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NOT_FOUND);
}

TEST(InventoryRepo, UpdateStorageChangesName)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage s;
    s.name = "Old";
    auto id = *repo.createStorage(s);
    s.id = id;
    s.name = "New";
    ASSERT_TRUE(repo.updateStorage(s).has_value());
    EXPECT_EQ(repo.getStorage(id)->name, "New");
}

TEST(InventoryRepo, UpdateUnknownStorageIsNotFound)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage s;
    s.id = 12345;
    s.name = "Phantom";
    auto r = repo.updateStorage(s);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NOT_FOUND);
}

TEST(InventoryRepo, UpdateStorageEmptyNameIsBadInput)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage s;
    s.name = "Old";
    auto id = *repo.createStorage(s);
    s.id = id;
    s.name = "";
    auto r = repo.updateStorage(s);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_BAD_INPUT);
}

TEST(InventoryRepo, DeleteEmptyStorage)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage s; s.name = "Empty";
    auto id = *repo.createStorage(s);
    ASSERT_TRUE(repo.deleteStorage(id).has_value());
    EXPECT_FALSE(repo.getStorage(id).has_value());
}

TEST(InventoryRepo, DeleteStorageWithChildIsNotEmpty)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage parent;  parent.name = "P";
    auto pid = *repo.createStorage(parent);
    inv::Storage child;   child.name = "C"; child.parent_id = pid;
    ASSERT_TRUE(repo.createStorage(child).has_value());

    auto r = repo.deleteStorage(pid);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NOT_EMPTY);
}

TEST(InventoryRepo, DeleteStorageWithStuffIsNotEmpty)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage s; s.name = "S";
    auto sid = *repo.createStorage(s);
    inv::Stuff t; t.name = "Thing"; t.storage_id = sid;
    ASSERT_TRUE(repo.createStuff(t).has_value());

    auto r = repo.deleteStorage(sid);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NOT_EMPTY);
}

TEST(InventoryRepo, DeleteUnknownStorageIsNotFound)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto r = repo.deleteStorage(999);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NOT_FOUND);
}

// ---- stuff -----------------------------------------------------------

TEST(InventoryRepo, CreateStuffRequiresStorage)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Stuff t; t.name = "Thing"; // no storage_id
    auto r = repo.createStuff(t);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_BAD_INPUT);
}

TEST(InventoryRepo, CreateStuffRequiresName)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage s; s.name = "X";
    auto sid = *repo.createStorage(s);
    inv::Stuff t; t.storage_id = sid;
    auto r = repo.createStuff(t);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_BAD_INPUT);
}

TEST(InventoryRepo, GetUnknownStuffIsNotFound)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto r = repo.getStuff(42);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NOT_FOUND);
}

TEST(InventoryRepo, ListInStorage)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto seeded = overseer::test::seedFamily(repo);

    // 20 stuffs cycle across 3 storages -> apartment gets 7, room 7,
    // shelf 6.
    auto in_apt = *repo.listInStorage(seeded.apartment_id);
    auto in_room = *repo.listInStorage(seeded.room_id);
    auto in_shelf = *repo.listInStorage(seeded.shelf_id);
    EXPECT_EQ(in_apt.size() + in_room.size() + in_shelf.size(), 20U);
    EXPECT_EQ(in_apt.size(), 7U);
    EXPECT_EQ(in_room.size(), 7U);
    EXPECT_EQ(in_shelf.size(), 6U);
}

TEST(InventoryRepo, UpdateStuffRenamesAndMovesAtomically)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage a, b; a.name = "A"; b.name = "B";
    auto aid = *repo.createStorage(a);
    auto bid = *repo.createStorage(b);
    inv::Stuff t; t.name = "thing"; t.storage_id = aid;
    auto tid = *repo.createStuff(t);

    inv::Stuff up;
    up.id = tid; up.name = "thing2"; up.storage_id = bid;
    ASSERT_TRUE(repo.updateStuff(up).has_value());

    auto got = *repo.getStuff(tid);
    EXPECT_EQ(got.name, "thing2");
    EXPECT_EQ(got.storage_id, bid);
    // The move via updateStuff went through moveStuff, so history
    // should have a single entry for the previous location.
    auto moves = *repo.recentMoves(tid);
    ASSERT_EQ(moves.size(), 1U);
    EXPECT_EQ(moves.front().storage_id, aid);
}

TEST(InventoryRepo, UpdateStuffBadInput)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Stuff up; up.id = 1; up.name = ""; up.storage_id = 1;
    auto r = repo.updateStuff(up);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_BAD_INPUT);
}

TEST(InventoryRepo, MoveStuffRecordsHistory)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage a, b; a.name = "A"; b.name = "B";
    auto a_id = *repo.createStorage(a);
    auto b_id = *repo.createStorage(b);

    inv::Stuff t; t.name = "thing"; t.storage_id = a_id;
    auto t_id = *repo.createStuff(t);

    ASSERT_TRUE(repo.moveStuff(t_id, b_id).has_value());
    auto moves = *repo.recentMoves(t_id);
    ASSERT_EQ(moves.size(), 1U);
    EXPECT_EQ(moves.front().storage_id, a_id);
    EXPECT_EQ(repo.getStuff(t_id)->storage_id, b_id);
}

TEST(InventoryRepo, MoveStuffSameStorageIsNoop)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage s; s.name = "S";
    auto sid = *repo.createStorage(s);
    inv::Stuff t; t.name = "thing"; t.storage_id = sid;
    auto tid = *repo.createStuff(t);
    ASSERT_TRUE(repo.moveStuff(tid, sid).has_value());
    EXPECT_EQ(repo.recentMoves(tid)->size(), 0U);
}

TEST(InventoryRepo, MoveTrimTriggerKeepsLastTen)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    std::vector<int64_t> storages;
    for(int i = 0; i < 12; ++i)
    {
        inv::Storage s;
        s.name = std::string("S") + std::to_string(i);
        storages.push_back(*repo.createStorage(s));
    }
    inv::Stuff t; t.name = "thing"; t.storage_id = storages[0];
    auto tid = *repo.createStuff(t);
    // 11 moves: from S0->S1, S1->S2, ..., S10->S11. That records 11
    // entries; the trigger should keep only the most recent 10.
    for(int i = 1; i <= 11; ++i)
    {
        ASSERT_TRUE(repo.moveStuff(tid, storages[static_cast<size_t>(i)])
                        .has_value());
    }
    auto moves = *repo.recentMoves(tid);
    EXPECT_EQ(moves.size(), 10U);
    // The most-recent move recorded was the one out of S10. The oldest
    // (S0) should have been trimmed.
    EXPECT_EQ(moves.front().storage_id, storages[10]);
}

TEST(InventoryRepo, DeleteStuff)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    inv::Storage s; s.name = "S";
    auto sid = *repo.createStorage(s);
    inv::Stuff t; t.name = "thing"; t.storage_id = sid;
    auto tid = *repo.createStuff(t);
    ASSERT_TRUE(repo.deleteStuff(tid).has_value());
    EXPECT_FALSE(repo.getStuff(tid).has_value());
}

TEST(InventoryRepo, DeleteUnknownStuffIsNotFound)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto r = repo.deleteStuff(99);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NOT_FOUND);
}

// ---- search ----------------------------------------------------------

TEST(InventoryRepo, SearchStuffByName)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto seeded = overseer::test::seedFamily(repo);

    auto hits = *repo.searchStuffs("Stuff");
    EXPECT_FALSE(hits.empty());
    // First seeded stuff is named "Stuff 00", which matches the prefix.
    bool found = false;
    for(const auto& s : hits)
    {
        if(s.id == seeded.stuff_ids.front()) { found = true; break; }
    }
    EXPECT_TRUE(found);
}

TEST(InventoryRepo, SearchStorageFindsApartment)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto seeded = overseer::test::seedFamily(repo);
    auto hits = *repo.searchStorages("Apartment");
    bool found = false;
    for(const auto& s : hits)
    {
        if(s.id == seeded.apartment_id) { found = true; break; }
    }
    EXPECT_TRUE(found);
}

TEST(InventoryRepo, SearchEmptyQueryReturnsEmpty)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    EXPECT_TRUE(repo.searchStorages("")->empty());
    EXPECT_TRUE(repo.searchStuffs("")->empty());
}

// ---- attachments -----------------------------------------------------

TEST(InventoryRepo, PutAndGetAttachmentRoundTrip)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto seeded = overseer::test::seedFamily(repo);
    // seedFamily inserted 5 attachments; pull the first one.
    auto a = repo.getAttachment(seeded.attachment_ids[0]);
    ASSERT_TRUE(a.has_value());
    EXPECT_EQ(a->mime, "image/avif");
    EXPECT_FALSE(a->bytes.empty());
}

TEST(InventoryRepo, PutEmptyAttachmentIsBadInput)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    std::span<const unsigned char> empty;
    auto r = repo.putAttachment(empty, "image/png");
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_BAD_INPUT);
}

TEST(InventoryRepo, GetUnknownAttachmentIsNotFound)
{
    auto db = makeMemDB();
    inv::InventoryRepo repo(*db);
    auto r = repo.getAttachment(123);
    ASSERT_FALSE(r.has_value());
    EXPECT_EQ(errMsg(r.error()), inv::ERR_NOT_FOUND);
}