import { beforeEach, describe, expect, it } from "bun:test";
import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { Hono } from "hono";
import * as schema from "../../src/db/schema.ts";
import { profileRoutes } from "../../src/server/routes/profiles.ts";
import { setupRoutes } from "../../src/server/routes/setups.ts";
import { updateProfile } from "../../src/server/services/profile.service.ts";
import { updateProfileSchema } from "../../src/shared/schemas.ts";
import { createTestDb } from "../helpers/db.ts";

type Db = Awaited<ReturnType<typeof createTestDb>>["db"];

/**
 * Creates a test app with authenticated routes.
 * Auth middleware is simulated by always injecting userId.
 */
async function createTestApp() {
	const { db, userId } = await createTestDb();
	const app = new Hono();

	// Inject db for all routes
	app.use("*", async (c, next) => {
		c.set("db", db);
		c.set("userId", userId);
		await next();
	});

	// Public routes
	app.route("/api/users", profileRoutes);

	// Profile update on auth routes (inline to avoid requireAuth in tests)
	app.put(
		"/api/auth/profile",
		zValidator("json", updateProfileSchema),
		async (c) => {
			const testDb = c.get("db");
			const uid = c.get("userId")!;
			const data = c.req.valid("json");
			const updated = await updateProfile(testDb, uid, data);
			if (!updated) return c.json({ error: "User not found" }, 404);
			return c.json(updated);
		},
	);

	// Setup routes including /:id/public
	app.route("/api/setups", setupRoutes);

	return { app, db, userId };
}

/**
 * Creates a test app WITHOUT auth — no userId injected.
 * Public routes should still work; protected routes should fail.
 */
async function createNoAuthTestApp() {
	const { db, userId } = await createTestDb();
	const app = new Hono();

	app.use("*", async (c, next) => {
		c.set("db", db);
		// No userId set — simulates unauthenticated request
		await next();
	});

	// Public routes (work without auth)
	app.route("/api/users", profileRoutes);

	// Protected profile update — simulates auth rejection
	app.put("/api/auth/profile", async (c) => {
		const uid = c.get("userId");
		if (!uid) return c.json({ error: "Authentication required" }, 401);
		return c.json({ error: "Unexpected" }, 500);
	});

	app.route("/api/setups", setupRoutes);

	return { app, db, userId };
}

describe("Profile Routes", () => {
	let app: Hono;
	let db: Db;
	let userId: number;

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

	describe("GET /api/users/:id/profile", () => {
		it("returns 200 with profile data without auth", async () => {
			// Set up profile data
			await db
				.update(schema.users)
				.set({ displayName: "Alice", bio: "Bikepacker" })
				.where(eq(schema.users.id, userId));

			const res = await app.request(`/api/users/${userId}/profile`);
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body.id).toBe(userId);
			expect(body.displayName).toBe("Alice");
			expect(body.bio).toBe("Bikepacker");
			expect(body.setups).toEqual([]);
		});

		it("includes only public setups", async () => {
			// Create public and private setups
			await db.insert(schema.setups).values([
				{ name: "Public Setup", userId, visibility: "public" },
				{ name: "Private Setup", userId, visibility: "private" },
			]);

			const res = await app.request(`/api/users/${userId}/profile`);
			expect(res.status).toBe(200);

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

		it("returns 404 for non-existent user", async () => {
			const res = await app.request("/api/users/99999/profile");
			expect(res.status).toBe(404);
		});

		it("returns 400 for invalid user ID", async () => {
			const res = await app.request("/api/users/abc/profile");
			expect(res.status).toBe(400);
		});
	});

	describe("PUT /api/auth/profile", () => {
		it("returns 200 with updated fields when authenticated", async () => {
			const res = await app.request("/api/auth/profile", {
				method: "PUT",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					displayName: "Alice",
					bio: "Loves bikepacking",
				}),
			});

			expect(res.status).toBe(200);
			const body = await res.json();
			expect(body.displayName).toBe("Alice");
			expect(body.bio).toBe("Loves bikepacking");
		});

		it("returns 401 without auth", async () => {
			const { app: noAuthApp } = await createNoAuthTestApp();

			const res = await noAuthApp.request("/api/auth/profile", {
				method: "PUT",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ displayName: "Alice" }),
			});

			expect(res.status).toBe(401);
		});
	});
});

describe("Public Setup Routes", () => {
	let app: Hono;
	let db: Db;
	let userId: number;

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

	describe("GET /api/setups/:id/public", () => {
		it("returns 200 for public setup without auth", async () => {
			const [setup] = await db
				.insert(schema.setups)
				.values({ name: "My Public Setup", userId, visibility: "public" })
				.returning();

			const res = await app.request(`/api/setups/${setup.id}/public`);
			expect(res.status).toBe(200);

			const body = await res.json();
			expect(body.name).toBe("My Public Setup");
			expect(body.visibility).toBe("public");
			expect(body.items).toBeDefined();
		});

		it("returns 200 for public setup with items", async () => {
			const [setup] = await db
				.insert(schema.setups)
				.values({ name: "Loaded Setup", userId, visibility: "public" })
				.returning();

			const [cat] = await db
				.select()
				.from(schema.categories)
				.where(eq(schema.categories.userId, userId));

			const [item] = await db
				.insert(schema.items)
				.values({
					name: "Tent",
					categoryId: cat.id,
					userId,
					weightGrams: 1200,
					priceCents: 30000,
				})
				.returning();

			await db.insert(schema.setupItems).values({
				setupId: setup.id,
				itemId: item.id,
			});

			const res = await app.request(`/api/setups/${setup.id}/public`);
			expect(res.status).toBe(200);

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

		it("returns 404 for private setup", async () => {
			const [setup] = await db
				.insert(schema.setups)
				.values({ name: "Private Setup", userId, visibility: "private" })
				.returning();

			const res = await app.request(`/api/setups/${setup.id}/public`);
			expect(res.status).toBe(404);
		});

		it("returns 404 for non-existent setup", async () => {
			const res = await app.request("/api/setups/99999/public");
			expect(res.status).toBe(404);
		});
	});
});
