import { beforeEach, describe, expect, it } from "bun:test";
import { eq } from "drizzle-orm";
import { items as itemsTable } from "../../src/db/schema.ts";
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 { createSecondTestUser, createTestDb } from "../helpers/db.ts";

describe("Setup Service", () => {
	let db: any;
	let userId: number;

	beforeEach(async () => {
		({ db, userId } = await createTestDb());
	});

	describe("createSetup", () => {
		it("creates setup with name, returns setup with id/timestamps", async () => {
			const setup = await createSetup(db, userId, { 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", async () => {
			const setup = await createSetup(db, userId, {
				name: "Backpacking",
			});
			const item1 = await createItem(db, userId, {
				name: "Tent",
				categoryId: 1,
				weightGrams: 1200,
				priceCents: 30000,
			});
			const item2 = await createItem(db, userId, {
				name: "Sleeping Bag",
				categoryId: 1,
				weightGrams: 800,
				priceCents: 20000,
			});
			await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]);

			const setups = await getAllSetups(db, userId);
			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", async () => {
			await createSetup(db, userId, { name: "Empty Setup" });

			const setups = await getAllSetups(db, userId);
			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", async () => {
			const setup = await createSetup(db, userId, { name: "Day Hike" });
			const item = await createItem(db, userId, {
				name: "Water Bottle",
				categoryId: 1,
				weightGrams: 200,
				priceCents: 2500,
			});
			await syncSetupItems(db, userId, setup.id, [item.id]);

			const result = await getSetupWithItems(db, userId, 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", async () => {
			const result = await getSetupWithItems(db, userId, 9999);
			expect(result).toBeNull();
		});
	});

	describe("updateSetup", () => {
		it("updates setup name, returns updated setup", async () => {
			const setup = await createSetup(db, userId, { name: "Original" });
			const updated = await updateSetup(db, userId, setup.id, {
				name: "Renamed",
			});

			expect(updated).toBeDefined();
			expect(updated?.name).toBe("Renamed");
		});

		it("returns null for non-existent setup", async () => {
			const result = await updateSetup(db, userId, 9999, {
				name: "Ghost",
			});
			expect(result).toBeNull();
		});
	});

	describe("deleteSetup", () => {
		it("removes setup and cascades to setup_items", async () => {
			const setup = await createSetup(db, userId, { name: "To Delete" });
			const item = await createItem(db, userId, {
				name: "Item",
				categoryId: 1,
			});
			await syncSetupItems(db, userId, setup.id, [item.id]);

			const deleted = await deleteSetup(db, userId, setup.id);
			expect(deleted).toBe(true);

			// Setup gone
			const result = await getSetupWithItems(db, userId, setup.id);
			expect(result).toBeNull();
		});

		it("returns false for non-existent setup", async () => {
			const result = await deleteSetup(db, userId, 9999);
			expect(result).toBe(false);
		});
	});

	describe("syncSetupItems", () => {
		it("sets items for a setup (delete-all + re-insert)", async () => {
			const setup = await createSetup(db, userId, { name: "Kit" });
			const item1 = await createItem(db, userId, {
				name: "Item 1",
				categoryId: 1,
			});
			const item2 = await createItem(db, userId, {
				name: "Item 2",
				categoryId: 1,
			});
			const item3 = await createItem(db, userId, {
				name: "Item 3",
				categoryId: 1,
			});

			// Initial sync
			await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]);
			let result = await getSetupWithItems(db, userId, setup.id);
			expect(result?.items).toHaveLength(2);

			// Re-sync with different items
			await syncSetupItems(db, userId, setup.id, [item2.id, item3.id]);
			result = await getSetupWithItems(db, userId, 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", async () => {
			const setup = await createSetup(db, userId, { name: "Kit" });
			const item = await createItem(db, userId, {
				name: "Item",
				categoryId: 1,
			});
			await syncSetupItems(db, userId, setup.id, [item.id]);

			await syncSetupItems(db, userId, setup.id, []);
			const result = await getSetupWithItems(db, userId, setup.id);
			expect(result?.items).toHaveLength(0);
		});
	});

	describe("removeSetupItem", () => {
		it("removes single item from setup", async () => {
			const setup = await createSetup(db, userId, { name: "Kit" });
			const item1 = await createItem(db, userId, {
				name: "Item 1",
				categoryId: 1,
			});
			const item2 = await createItem(db, userId, {
				name: "Item 2",
				categoryId: 1,
			});
			await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]);

			await removeSetupItem(db, userId, setup.id, item1.id);
			const result = await getSetupWithItems(db, userId, 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", async () => {
			const setup = await createSetup(db, userId, { name: "Day Hike" });
			const item = await createItem(db, userId, {
				name: "Water Bottle",
				categoryId: 1,
				weightGrams: 200,
				priceCents: 2500,
			});
			await syncSetupItems(db, userId, setup.id, [item.id]);

			const result = await getSetupWithItems(db, userId, 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", async () => {
			const setup = await createSetup(db, userId, { name: "Kit" });
			const item1 = await createItem(db, userId, {
				name: "Tent",
				categoryId: 1,
			});
			const item2 = await createItem(db, userId, {
				name: "Jacket",
				categoryId: 1,
			});
			const item3 = await createItem(db, userId, {
				name: "Stove",
				categoryId: 1,
			});

			// Initial sync
			await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]);

			// Change classifications
			await updateItemClassification(db, userId, setup.id, item1.id, "worn");
			await updateItemClassification(
				db,
				userId,
				setup.id,
				item2.id,
				"consumable",
			);

			// Re-sync with item2 kept and item3 added (item1 removed)
			await syncSetupItems(db, userId, setup.id, [item2.id, item3.id]);

			const result = await getSetupWithItems(db, userId, 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", async () => {
			const setup = await createSetup(db, userId, { name: "Kit" });
			const item1 = await createItem(db, userId, {
				name: "Item 1",
				categoryId: 1,
			});

			await syncSetupItems(db, userId, setup.id, [item1.id]);
			const result = await getSetupWithItems(db, userId, setup.id);
			expect(result?.items[0].classification).toBe("base");
		});
	});

	describe("updateItemClassification", () => {
		it("sets classification for a specific item in a specific setup", async () => {
			const setup = await createSetup(db, userId, { name: "Kit" });
			const item = await createItem(db, userId, {
				name: "Tent",
				categoryId: 1,
			});
			await syncSetupItems(db, userId, setup.id, [item.id]);

			await updateItemClassification(db, userId, setup.id, item.id, "worn");

			const result = await getSetupWithItems(db, userId, setup.id);
			expect(result?.items[0].classification).toBe("worn");
		});

		it("changes item from default 'base' to 'worn'", async () => {
			const setup = await createSetup(db, userId, { name: "Kit" });
			const item = await createItem(db, userId, {
				name: "Jacket",
				categoryId: 1,
			});
			await syncSetupItems(db, userId, setup.id, [item.id]);

			// Verify default
			let result = await getSetupWithItems(db, userId, setup.id);
			expect(result?.items[0].classification).toBe("base");

			// Update
			await updateItemClassification(db, userId, setup.id, item.id, "worn");

			result = await getSetupWithItems(db, userId, setup.id);
			expect(result?.items[0].classification).toBe("worn");
		});

		it("same item in two different setups can have different classifications", async () => {
			const setup1 = await createSetup(db, userId, { name: "Hiking" });
			const setup2 = await createSetup(db, userId, { name: "Biking" });
			const item = await createItem(db, userId, {
				name: "Jacket",
				categoryId: 1,
			});

			await syncSetupItems(db, userId, setup1.id, [item.id]);
			await syncSetupItems(db, userId, setup2.id, [item.id]);

			await updateItemClassification(db, userId, setup1.id, item.id, "worn");
			await updateItemClassification(db, userId, setup2.id, item.id, "base");

			const result1 = await getSetupWithItems(db, userId, setup1.id);
			const result2 = await getSetupWithItems(db, userId, 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", async () => {
			const setup = await createSetup(db, userId, { name: "Kit" });
			const item1 = await createItem(db, userId, {
				name: "Item 1",
				categoryId: 1,
			});
			const item2 = await createItem(db, userId, {
				name: "Item 2",
				categoryId: 1,
			});
			await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]);

			// Delete item1 from collection
			await db.delete(itemsTable).where(eq(itemsTable.id, item1.id));

			const result = await getSetupWithItems(db, userId, setup.id);
			expect(result?.items).toHaveLength(1);
			expect(result?.items[0].name).toBe("Item 2");
		});
	});

	describe("cross-user isolation", () => {
		it("user cannot see other user's setups", async () => {
			const userId2 = await createSecondTestUser(db);

			await createSetup(db, userId, { name: "User 1 Setup" });
			await createSetup(db, userId2, { name: "User 2 Setup" });

			const user1Setups = await getAllSetups(db, userId);
			const user2Setups = await getAllSetups(db, userId2);

			expect(user1Setups).toHaveLength(1);
			expect(user1Setups[0].name).toBe("User 1 Setup");
			expect(user2Setups).toHaveLength(1);
			expect(user2Setups[0].name).toBe("User 2 Setup");
		});
	});
});
