import { beforeEach, describe, expect, it } from "bun:test";
import { Hono } from "hono";
import { globalItems, setups } from "../../src/db/schema.ts";
import { discoveryRoutes } from "../../src/server/routes/discovery.ts";
import { createTestDb } from "../helpers/db.ts";

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

async function createTestApp() {
	const { db, userId } = await createTestDb();
	const app = new Hono();

	app.use("*", async (c, next) => {
		c.set("db", db);
		// Note: NO userId set — discovery endpoints don't need auth
		await next();
	});

	app.route("/api/discovery", discoveryRoutes);
	return { app, db, userId };
}

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

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

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

	beforeEach(async () => {
		const testApp = await createTestApp();
		app = testApp.app;
		db = testApp.db;
		userId = testApp.userId;
	});

	describe("GET /api/discovery/setups", () => {
		it("returns 200 with { items, nextCursor, hasMore } shape", async () => {
			await insertPublicSetup(db, userId, "My Bikepacking Setup");

			const res = await app.request("/api/discovery/setups");
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body).toHaveProperty("items");
			expect(body).toHaveProperty("nextCursor");
			expect(body).toHaveProperty("hasMore");
			expect(Array.isArray(body.items)).toBe(true);
		});

		it("returns only public setups", async () => {
			await insertPublicSetup(db, userId, "Public Setup");
			// Insert a private setup
			await db
				.insert(setups)
				.values({ name: "Private Setup", userId, visibility: "private" });

			const res = await app.request("/api/discovery/setups");
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body.items).toHaveLength(1);
			expect(body.items[0].name).toBe("Public Setup");
		});

		it("respects limit query param", async () => {
			for (let i = 0; i < 5; i++) {
				await insertPublicSetup(db, userId, `Setup ${i}`);
			}

			const res = await app.request("/api/discovery/setups?limit=2");
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body.items).toHaveLength(2);
			expect(body.hasMore).toBe(true);
		});
	});

	describe("GET /api/discovery/items", () => {
		it("returns 200 with { items, nextCursor, hasMore } shape", async () => {
			await insertGlobalItem(db, "MSR", "PocketRocket 2");

			const res = await app.request("/api/discovery/items");
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body).toHaveProperty("items");
			expect(body).toHaveProperty("nextCursor");
			expect(body).toHaveProperty("hasMore");
			expect(Array.isArray(body.items)).toBe(true);
		});

		it("returns 200 with empty items when catalog is empty", async () => {
			const res = await app.request("/api/discovery/items");
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body.items).toHaveLength(0);
			expect(body.hasMore).toBe(false);
			expect(body.nextCursor).toBeNull();
		});

		it("respects limit query param", async () => {
			for (let i = 0; i < 5; i++) {
				await insertGlobalItem(db, `Brand${i}`, `Model${i}`);
			}

			const res = await app.request("/api/discovery/items?limit=2");
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body.items).toHaveLength(2);
			expect(body.hasMore).toBe(true);
		});

		it("pagination with cursor returns items before cursor timestamp", async () => {
			// Insert items with explicit timestamps so they are definitively ordered
			const olderTime = new Date("2024-01-01T00:00:00Z");
			const newerTime = new Date("2024-06-01T00:00:00Z");

			await db.insert(globalItems).values({
				brand: "Brand A",
				model: "Model A",
				category: "bags",
				createdAt: olderTime,
			});
			await db.insert(globalItems).values({
				brand: "Brand B",
				model: "Model B",
				category: "bags",
				createdAt: newerTime,
			});

			// First page (newest first: Brand B)
			const res1 = await app.request("/api/discovery/items?limit=1");
			expect(res1.status).toBe(200);
			const page1 = await res1.json();
			expect(page1.items).toHaveLength(1);
			expect(page1.items[0].brand).toBe("Brand B");
			expect(page1.hasMore).toBe(true);
			expect(page1.nextCursor).not.toBeNull();

			// Second page using cursor (should return Brand A)
			const cursor = encodeURIComponent(page1.nextCursor);
			const res2 = await app.request(
				`/api/discovery/items?limit=1&cursor=${cursor}`,
			);
			expect(res2.status).toBe(200);
			const page2 = await res2.json();
			expect(page2.items).toHaveLength(1);
			expect(page2.items[0].brand).toBe("Brand A");
		});
	});

	describe("GET /api/discovery/categories", () => {
		it("returns 200 with array shape", async () => {
			await insertGlobalItem(db, "MSR", "PocketRocket", "cooking");
			await insertGlobalItem(db, "Revelate", "Terrapin", "bags");

			const res = await app.request("/api/discovery/categories");
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(Array.isArray(body)).toBe(true);
		});

		it("returns categories with name and itemCount fields", async () => {
			await insertGlobalItem(db, "MSR", "PocketRocket", "cooking");
			await insertGlobalItem(db, "Jetboil", "Flash", "cooking");
			await insertGlobalItem(db, "Revelate", "Terrapin", "bags");

			const res = await app.request("/api/discovery/categories");
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body.length).toBeGreaterThan(0);
			expect(body[0]).toHaveProperty("name");
			expect(body[0]).toHaveProperty("itemCount");
			// cooking has most items (2), should be first
			expect(body[0].name).toBe("cooking");
			expect(body[0].itemCount).toBe(2);
		});

		it("respects limit query param", async () => {
			await insertGlobalItem(db, "Brand A", "Model A", "category-a");
			await insertGlobalItem(db, "Brand B", "Model B", "category-b");
			await insertGlobalItem(db, "Brand C", "Model C", "category-c");

			const res = await app.request("/api/discovery/categories?limit=2");
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body).toHaveLength(2);
		});
	});
});
