import { beforeEach, describe, expect, it } from "bun:test"; import { createItem } from "../../src/server/services/item.service.ts"; import { createSetup, deleteSetup, getAllSetups, getSetupWithItems, removeSetupItem, syncSetupItems, updateItemClassification, updateSetup, } from "../../src/server/services/setup.service.ts"; import { createTestDb } from "../helpers/db.ts"; describe("Setup Service", () => { let db: ReturnType; beforeEach(() => { db = createTestDb(); }); describe("createSetup", () => { it("creates setup with name, returns setup with id/timestamps", () => { const setup = createSetup(db, { name: "Day Hike" }); expect(setup).toBeDefined(); expect(setup.id).toBeGreaterThan(0); expect(setup.name).toBe("Day Hike"); expect(setup.createdAt).toBeDefined(); expect(setup.updatedAt).toBeDefined(); }); }); describe("getAllSetups", () => { it("returns setups with itemCount, totalWeight, totalCost", () => { const setup = createSetup(db, { name: "Backpacking" }); const item1 = createItem(db, { name: "Tent", categoryId: 1, weightGrams: 1200, priceCents: 30000, }); const item2 = createItem(db, { name: "Sleeping Bag", categoryId: 1, weightGrams: 800, priceCents: 20000, }); syncSetupItems(db, setup.id, [item1.id, item2.id]); const setups = getAllSetups(db); expect(setups).toHaveLength(1); expect(setups[0].name).toBe("Backpacking"); expect(setups[0].itemCount).toBe(2); expect(setups[0].totalWeight).toBe(2000); expect(setups[0].totalCost).toBe(50000); }); it("returns 0 for weight/cost when setup has no items", () => { createSetup(db, { name: "Empty Setup" }); const setups = getAllSetups(db); expect(setups).toHaveLength(1); expect(setups[0].itemCount).toBe(0); expect(setups[0].totalWeight).toBe(0); expect(setups[0].totalCost).toBe(0); }); }); describe("getSetupWithItems", () => { it("returns setup with full item details and category info", () => { const setup = createSetup(db, { name: "Day Hike" }); const item = createItem(db, { name: "Water Bottle", categoryId: 1, weightGrams: 200, priceCents: 2500, }); syncSetupItems(db, setup.id, [item.id]); const result = getSetupWithItems(db, setup.id); expect(result).toBeDefined(); expect(result?.name).toBe("Day Hike"); expect(result?.items).toHaveLength(1); expect(result?.items[0].name).toBe("Water Bottle"); expect(result?.items[0].categoryName).toBe("Uncategorized"); expect(result?.items[0].categoryIcon).toBeDefined(); }); it("returns null for non-existent setup", () => { const result = getSetupWithItems(db, 9999); expect(result).toBeNull(); }); }); describe("updateSetup", () => { it("updates setup name, returns updated setup", () => { const setup = createSetup(db, { name: "Original" }); const updated = updateSetup(db, setup.id, { name: "Renamed" }); expect(updated).toBeDefined(); expect(updated?.name).toBe("Renamed"); }); it("returns null for non-existent setup", () => { const result = updateSetup(db, 9999, { name: "Ghost" }); expect(result).toBeNull(); }); }); describe("deleteSetup", () => { it("removes setup and cascades to setup_items", () => { const setup = createSetup(db, { name: "To Delete" }); const item = createItem(db, { name: "Item", categoryId: 1 }); syncSetupItems(db, setup.id, [item.id]); const deleted = deleteSetup(db, setup.id); expect(deleted).toBe(true); // Setup gone const result = getSetupWithItems(db, setup.id); expect(result).toBeNull(); }); it("returns false for non-existent setup", () => { const result = deleteSetup(db, 9999); expect(result).toBe(false); }); }); describe("syncSetupItems", () => { it("sets items for a setup (delete-all + re-insert)", () => { const setup = createSetup(db, { name: "Kit" }); const item1 = createItem(db, { name: "Item 1", categoryId: 1 }); const item2 = createItem(db, { name: "Item 2", categoryId: 1 }); const item3 = createItem(db, { name: "Item 3", categoryId: 1 }); // Initial sync syncSetupItems(db, setup.id, [item1.id, item2.id]); let result = getSetupWithItems(db, setup.id); expect(result?.items).toHaveLength(2); // Re-sync with different items syncSetupItems(db, setup.id, [item2.id, item3.id]); result = getSetupWithItems(db, setup.id); expect(result?.items).toHaveLength(2); const names = result?.items.map((i: any) => i.name).sort(); expect(names).toEqual(["Item 2", "Item 3"]); }); it("syncing with empty array clears all items", () => { const setup = createSetup(db, { name: "Kit" }); const item = createItem(db, { name: "Item", categoryId: 1 }); syncSetupItems(db, setup.id, [item.id]); syncSetupItems(db, setup.id, []); const result = getSetupWithItems(db, setup.id); expect(result?.items).toHaveLength(0); }); }); describe("removeSetupItem", () => { it("removes single item from setup", () => { const setup = createSetup(db, { name: "Kit" }); const item1 = createItem(db, { name: "Item 1", categoryId: 1 }); const item2 = createItem(db, { name: "Item 2", categoryId: 1 }); syncSetupItems(db, setup.id, [item1.id, item2.id]); removeSetupItem(db, setup.id, item1.id); const result = getSetupWithItems(db, setup.id); expect(result?.items).toHaveLength(1); expect(result?.items[0].name).toBe("Item 2"); }); }); describe("getSetupWithItems - classification", () => { it("returns classification field defaulting to 'base' for each item", () => { const setup = createSetup(db, { name: "Day Hike" }); const item = createItem(db, { name: "Water Bottle", categoryId: 1, weightGrams: 200, priceCents: 2500, }); syncSetupItems(db, setup.id, [item.id]); const result = getSetupWithItems(db, setup.id); expect(result?.items).toHaveLength(1); expect(result?.items[0].classification).toBe("base"); }); }); describe("syncSetupItems - classification preservation", () => { it("preserves existing classifications when re-syncing items", () => { const setup = createSetup(db, { name: "Kit" }); const item1 = createItem(db, { name: "Tent", categoryId: 1 }); const item2 = createItem(db, { name: "Jacket", categoryId: 1 }); const item3 = createItem(db, { name: "Stove", categoryId: 1 }); // Initial sync syncSetupItems(db, setup.id, [item1.id, item2.id]); // Change classifications updateItemClassification(db, setup.id, item1.id, "worn"); updateItemClassification(db, setup.id, item2.id, "consumable"); // Re-sync with item2 kept and item3 added (item1 removed) syncSetupItems(db, setup.id, [item2.id, item3.id]); const result = getSetupWithItems(db, setup.id); expect(result?.items).toHaveLength(2); const item2Result = result?.items.find((i: any) => i.name === "Jacket"); const item3Result = result?.items.find((i: any) => i.name === "Stove"); expect(item2Result?.classification).toBe("consumable"); expect(item3Result?.classification).toBe("base"); }); it("assigns 'base' to newly added items with no prior classification", () => { const setup = createSetup(db, { name: "Kit" }); const item1 = createItem(db, { name: "Item 1", categoryId: 1 }); syncSetupItems(db, setup.id, [item1.id]); const result = getSetupWithItems(db, setup.id); expect(result?.items[0].classification).toBe("base"); }); }); describe("updateItemClassification", () => { it("sets classification for a specific item in a specific setup", () => { const setup = createSetup(db, { name: "Kit" }); const item = createItem(db, { name: "Tent", categoryId: 1 }); syncSetupItems(db, setup.id, [item.id]); updateItemClassification(db, setup.id, item.id, "worn"); const result = getSetupWithItems(db, setup.id); expect(result?.items[0].classification).toBe("worn"); }); it("changes item from default 'base' to 'worn'", () => { const setup = createSetup(db, { name: "Kit" }); const item = createItem(db, { name: "Jacket", categoryId: 1 }); syncSetupItems(db, setup.id, [item.id]); // Verify default let result = getSetupWithItems(db, setup.id); expect(result?.items[0].classification).toBe("base"); // Update updateItemClassification(db, setup.id, item.id, "worn"); result = getSetupWithItems(db, setup.id); expect(result?.items[0].classification).toBe("worn"); }); it("same item in two different setups can have different classifications", () => { const setup1 = createSetup(db, { name: "Hiking" }); const setup2 = createSetup(db, { name: "Biking" }); const item = createItem(db, { name: "Jacket", categoryId: 1 }); syncSetupItems(db, setup1.id, [item.id]); syncSetupItems(db, setup2.id, [item.id]); updateItemClassification(db, setup1.id, item.id, "worn"); updateItemClassification(db, setup2.id, item.id, "base"); const result1 = getSetupWithItems(db, setup1.id); const result2 = getSetupWithItems(db, setup2.id); expect(result1?.items[0].classification).toBe("worn"); expect(result2?.items[0].classification).toBe("base"); }); }); describe("cascade behavior", () => { it("deleting a collection item removes it from all setups", () => { const setup = createSetup(db, { name: "Kit" }); const item1 = createItem(db, { name: "Item 1", categoryId: 1 }); const item2 = createItem(db, { name: "Item 2", categoryId: 1 }); syncSetupItems(db, setup.id, [item1.id, item2.id]); // Delete item1 from collection (need direct DB access) const { items: itemsTable } = require("../../src/db/schema.ts"); const { eq } = require("drizzle-orm"); db.delete(itemsTable).where(eq(itemsTable.id, item1.id)).run(); const result = getSetupWithItems(db, setup.id); expect(result?.items).toHaveLength(1); expect(result?.items[0].name).toBe("Item 2"); }); }); });