import { beforeEach, describe, expect, it } from "bun:test";
import { eq } from "drizzle-orm";
import {
	globalItems,
	items,
	setupItems,
	setups,
	users,
} from "../../src/db/schema.ts";
import {
	getPopularSetups,
	getRecentGlobalItems,
	getTrendingCategories,
} from "../../src/server/services/discovery.service.ts";
import { createTestDb } from "../helpers/db.ts";

type TestDb = Awaited<ReturnType<typeof createTestDb>>;

async function insertGlobalItem(
	db: TestDb["db"],
	data: { brand: string; model: string; category?: string },
) {
	const [row] = await db
		.insert(globalItems)
		.values({
			brand: data.brand,
			model: data.model,
			category: data.category ?? null,
		})
		.returning();
	return row;
}

async function insertItem(db: TestDb["db"], userId: number, categoryId = 1) {
	const [row] = await db
		.insert(items)
		.values({ name: "Test Item", categoryId, userId })
		.returning();
	return row;
}

async function insertPublicSetup(
	db: TestDb["db"],
	userId: number,
	name: string,
	itemIds: number[],
) {
	const [setup] = await db
		.insert(setups)
		.values({ name, userId, visibility: "public" })
		.returning();
	for (const itemId of itemIds) {
		await db.insert(setupItems).values({ setupId: setup.id, itemId });
	}
	return setup;
}

async function insertPrivateSetup(
	db: TestDb["db"],
	userId: number,
	name: string,
) {
	const [setup] = await db
		.insert(setups)
		.values({ name, userId, visibility: "private" })
		.returning();
	return setup;
}

describe("Discovery Service", () => {
	let db: TestDb["db"];
	let userId: number;

	beforeEach(async () => {
		const testDb = await createTestDb();
		db = testDb.db;
		userId = testDb.userId;
	});

	describe("getPopularSetups", () => {
		it("returns public setups ordered by item count descending", async () => {
			const item1 = await insertItem(db, userId);
			const item2 = await insertItem(db, userId);
			const item3 = await insertItem(db, userId);

			// Setup with 1 item
			await insertPublicSetup(db, userId, "Solo Setup", [item1.id]);
			// Setup with 2 items
			await insertPublicSetup(db, userId, "Dual Setup", [item2.id, item3.id]);

			const result = await getPopularSetups(db);
			expect(result.items).toHaveLength(2);
			expect(result.items[0].name).toBe("Dual Setup");
			expect(result.items[0].itemCount).toBe(2);
			expect(result.items[1].name).toBe("Solo Setup");
			expect(result.items[1].itemCount).toBe(1);
		});

		it("excludes private setups", async () => {
			const item1 = await insertItem(db, userId);
			await insertPublicSetup(db, userId, "Public Setup", [item1.id]);
			await insertPrivateSetup(db, userId, "Private Setup");

			const result = await getPopularSetups(db);
			expect(result.items).toHaveLength(1);
			expect(result.items[0].name).toBe("Public Setup");
		});

		it("returns hasMore=true and nextCursor when more results exist", async () => {
			const item1 = await insertItem(db, userId);
			const item2 = await insertItem(db, userId);
			const item3 = await insertItem(db, userId);

			await insertPublicSetup(db, userId, "Setup A", [
				item1.id,
				item2.id,
				item3.id,
			]);
			await insertPublicSetup(db, userId, "Setup B", [item1.id, item2.id]);
			await insertPublicSetup(db, userId, "Setup C", [item1.id]);

			const result = await getPopularSetups(db, 1);
			expect(result.hasMore).toBe(true);
			expect(result.nextCursor).not.toBeNull();
		});

		it("returns second page without duplicates when cursor provided", async () => {
			const item1 = await insertItem(db, userId);
			const item2 = await insertItem(db, userId);
			const item3 = await insertItem(db, userId);

			await insertPublicSetup(db, userId, "Setup A", [
				item1.id,
				item2.id,
				item3.id,
			]);
			await insertPublicSetup(db, userId, "Setup B", [item1.id, item2.id]);
			await insertPublicSetup(db, userId, "Setup C", [item1.id]);

			const page1 = await getPopularSetups(db, 1);
			expect(page1.items).toHaveLength(1);
			expect(page1.nextCursor).not.toBeNull();

			const page2 = await getPopularSetups(db, 1, page1.nextCursor!);
			expect(page2.items).toHaveLength(1);
			expect(page2.items[0].id).not.toBe(page1.items[0].id);
		});

		it("includes creatorName from users.displayName", async () => {
			// Update user display name
			await db
				.update(users)
				.set({ displayName: "Jean-Luc" })
				.where(eq(users.id, userId));

			const item1 = await insertItem(db, userId);
			await insertPublicSetup(db, userId, "My Setup", [item1.id]);

			const result = await getPopularSetups(db);
			expect(result.items).toHaveLength(1);
			expect(result.items[0].creatorName).toBe("Jean-Luc");
		});
	});

	describe("getRecentGlobalItems", () => {
		it("returns items ordered by createdAt descending", async () => {
			// Insert items with slight delay to get different timestamps
			const item1 = await insertGlobalItem(db, {
				brand: "BrandA",
				model: "Model1",
			});
			await new Promise((r) => setTimeout(r, 5));
			const item2 = await insertGlobalItem(db, {
				brand: "BrandB",
				model: "Model2",
			});
			await new Promise((r) => setTimeout(r, 5));
			const item3 = await insertGlobalItem(db, {
				brand: "BrandC",
				model: "Model3",
			});

			const result = await getRecentGlobalItems(db);
			expect(result.items).toHaveLength(3);
			// Most recent first
			expect(result.items[0].id).toBe(item3.id);
			expect(result.items[1].id).toBe(item2.id);
			expect(result.items[2].id).toBe(item1.id);
		});

		it("returns hasMore=true and nextCursor when more results exist", async () => {
			await insertGlobalItem(db, { brand: "BrandA", model: "Model1" });
			await new Promise((r) => setTimeout(r, 5));
			await insertGlobalItem(db, { brand: "BrandB", model: "Model2" });
			await new Promise((r) => setTimeout(r, 5));
			await insertGlobalItem(db, { brand: "BrandC", model: "Model3" });

			const result = await getRecentGlobalItems(db, 2);
			expect(result.hasMore).toBe(true);
			expect(result.nextCursor).not.toBeNull();
		});

		it("returns second page without duplicates when cursor provided", async () => {
			await insertGlobalItem(db, { brand: "BrandA", model: "Model1" });
			await new Promise((r) => setTimeout(r, 5));
			await insertGlobalItem(db, { brand: "BrandB", model: "Model2" });
			await new Promise((r) => setTimeout(r, 5));
			await insertGlobalItem(db, { brand: "BrandC", model: "Model3" });

			const page1 = await getRecentGlobalItems(db, 2);
			expect(page1.items).toHaveLength(2);
			expect(page1.nextCursor).not.toBeNull();

			const page2 = await getRecentGlobalItems(db, 2, page1.nextCursor!);
			expect(page2.items).toHaveLength(1);
			// Page 2 item should not appear in page 1
			const page1Ids = page1.items.map((i) => i.id);
			expect(page1Ids).not.toContain(page2.items[0].id);
		});
	});

	describe("getTrendingCategories", () => {
		it("returns categories ordered by item count descending", async () => {
			// 3 items in Tents, 1 in Bags, 2 in Stoves
			await insertGlobalItem(db, {
				brand: "BrandA",
				model: "Tent1",
				category: "Tents",
			});
			await insertGlobalItem(db, {
				brand: "BrandB",
				model: "Tent2",
				category: "Tents",
			});
			await insertGlobalItem(db, {
				brand: "BrandC",
				model: "Tent3",
				category: "Tents",
			});
			await insertGlobalItem(db, {
				brand: "BrandD",
				model: "Bag1",
				category: "Bags",
			});
			await insertGlobalItem(db, {
				brand: "BrandE",
				model: "Stove1",
				category: "Stoves",
			});
			await insertGlobalItem(db, {
				brand: "BrandF",
				model: "Stove2",
				category: "Stoves",
			});

			const result = await getTrendingCategories(db);
			expect(result).toHaveLength(3);
			expect(result[0].name).toBe("Tents");
			expect(result[0].itemCount).toBe(3);
			expect(result[1].name).toBe("Stoves");
			expect(result[1].itemCount).toBe(2);
			expect(result[2].name).toBe("Bags");
			expect(result[2].itemCount).toBe(1);
		});

		it("excludes items with null category", async () => {
			await insertGlobalItem(db, {
				brand: "BrandA",
				model: "Tent1",
				category: "Tents",
			});
			// No category — should be excluded
			await insertGlobalItem(db, { brand: "BrandB", model: "NoCategory" });

			const result = await getTrendingCategories(db);
			expect(result).toHaveLength(1);
			expect(result[0].name).toBe("Tents");
		});

		it("returns empty array when no items have a category set", async () => {
			await insertGlobalItem(db, { brand: "BrandA", model: "Model1" });
			await insertGlobalItem(db, { brand: "BrandB", model: "Model2" });

			const result = await getTrendingCategories(db);
			expect(result).toHaveLength(0);
		});
	});
});
