import { beforeEach, describe, expect, it } from "bun:test";
import { Hono } from "hono";
import { threadRoutes } from "../../src/server/routes/threads.ts";
import { createTestDb } from "../helpers/db.ts";

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

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

	app.route("/api/threads", threadRoutes);
	return { app, db, userId };
}

async function createThreadViaAPI(app: Hono, name: string, categoryId = 1) {
	const res = await app.request("/api/threads", {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify({ name, categoryId }),
	});
	return res.json();
}

async function createCandidateViaAPI(app: Hono, threadId: number, data: any) {
	const res = await app.request(`/api/threads/${threadId}/candidates`, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify(data),
	});
	return res.json();
}

describe("Thread Routes", () => {
	let app: Hono;

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

	describe("POST /api/threads", () => {
		it("with valid body returns 201 + thread object", async () => {
			const res = await app.request("/api/threads", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ name: "New Tent", categoryId: 1 }),
			});

			expect(res.status).toBe(201);
			const body = await res.json();
			expect(body.name).toBe("New Tent");
			expect(body.id).toBeGreaterThan(0);
			expect(body.status).toBe("active");
		});

		it("with empty name returns 400", async () => {
			const res = await app.request("/api/threads", {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ name: "" }),
			});

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

	describe("GET /api/threads", () => {
		it("returns array of active threads with metadata", async () => {
			const thread = await createThreadViaAPI(app, "Backpack Options");
			await createCandidateViaAPI(app, thread.id, {
				name: "Pack A",
				categoryId: 1,
				priceCents: 20000,
			});

			const res = await app.request("/api/threads");
			expect(res.status).toBe(200);
			const body = await res.json();
			expect(Array.isArray(body)).toBe(true);
			expect(body.length).toBeGreaterThanOrEqual(1);
			expect(body[0].candidateCount).toBeDefined();
		});

		it("?includeResolved=true includes archived threads", async () => {
			const _t1 = await createThreadViaAPI(app, "Active");
			const t2 = await createThreadViaAPI(app, "To Resolve");
			const candidate = await createCandidateViaAPI(app, t2.id, {
				name: "Winner",
				categoryId: 1,
			});

			// Resolve thread
			await app.request(`/api/threads/${t2.id}/resolve`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ candidateId: candidate.id }),
			});

			// Default excludes resolved
			const defaultRes = await app.request("/api/threads");
			const defaultBody = await defaultRes.json();
			expect(defaultBody).toHaveLength(1);

			// With includeResolved includes all
			const allRes = await app.request("/api/threads?includeResolved=true");
			const allBody = await allRes.json();
			expect(allBody).toHaveLength(2);
		});
	});

	describe("GET /api/threads/:id", () => {
		it("returns thread with candidates", async () => {
			const thread = await createThreadViaAPI(app, "Tent Options");
			await createCandidateViaAPI(app, thread.id, {
				name: "Tent A",
				categoryId: 1,
				priceCents: 30000,
			});

			const res = await app.request(`/api/threads/${thread.id}`);
			expect(res.status).toBe(200);
			const body = await res.json();
			expect(body.name).toBe("Tent Options");
			expect(body.candidates).toHaveLength(1);
			expect(body.candidates[0].name).toBe("Tent A");
		});

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

	describe("PUT /api/threads/:id", () => {
		it("updates thread name", async () => {
			const thread = await createThreadViaAPI(app, "Original");

			const res = await app.request(`/api/threads/${thread.id}`, {
				method: "PUT",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ name: "Renamed" }),
			});

			expect(res.status).toBe(200);
			const body = await res.json();
			expect(body.name).toBe("Renamed");
		});
	});

	describe("DELETE /api/threads/:id", () => {
		it("removes thread", async () => {
			const thread = await createThreadViaAPI(app, "To Delete");

			const res = await app.request(`/api/threads/${thread.id}`, {
				method: "DELETE",
			});

			expect(res.status).toBe(200);
			const body = await res.json();
			expect(body.success).toBe(true);

			// Verify gone
			const getRes = await app.request(`/api/threads/${thread.id}`);
			expect(getRes.status).toBe(404);
		});
	});

	describe("POST /api/threads/:id/candidates", () => {
		it("adds candidate, returns 201", async () => {
			const thread = await createThreadViaAPI(app, "Test");

			const res = await app.request(`/api/threads/${thread.id}/candidates`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({
					name: "Candidate A",
					categoryId: 1,
					priceCents: 25000,
					weightGrams: 500,
				}),
			});

			expect(res.status).toBe(201);
			const body = await res.json();
			expect(body.name).toBe("Candidate A");
			expect(body.threadId).toBe(thread.id);
		});
	});

	describe("PUT /api/threads/:threadId/candidates/:candidateId", () => {
		it("updates candidate", async () => {
			const thread = await createThreadViaAPI(app, "Test");
			const candidate = await createCandidateViaAPI(app, thread.id, {
				name: "Original",
				categoryId: 1,
			});

			const res = await app.request(
				`/api/threads/${thread.id}/candidates/${candidate.id}`,
				{
					method: "PUT",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({ name: "Updated" }),
				},
			);

			expect(res.status).toBe(200);
			const body = await res.json();
			expect(body.name).toBe("Updated");
		});
	});

	describe("DELETE /api/threads/:threadId/candidates/:candidateId", () => {
		it("removes candidate", async () => {
			const thread = await createThreadViaAPI(app, "Test");
			const candidate = await createCandidateViaAPI(app, thread.id, {
				name: "To Remove",
				categoryId: 1,
			});

			const res = await app.request(
				`/api/threads/${thread.id}/candidates/${candidate.id}`,
				{ method: "DELETE" },
			);

			expect(res.status).toBe(200);
			const body = await res.json();
			expect(body.success).toBe(true);
		});
	});

	describe("PATCH /api/threads/:id/candidates/reorder", () => {
		it("with valid orderedIds returns 200 + { success: true }", async () => {
			const thread = await createThreadViaAPI(app, "Reorder Test");
			const c1 = await createCandidateViaAPI(app, thread.id, {
				name: "Candidate A",
				categoryId: 1,
			});
			const c2 = await createCandidateViaAPI(app, thread.id, {
				name: "Candidate B",
				categoryId: 1,
			});

			const res = await app.request(
				`/api/threads/${thread.id}/candidates/reorder`,
				{
					method: "PATCH",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({ orderedIds: [c2.id, c1.id] }),
				},
			);

			expect(res.status).toBe(200);
			const body = await res.json();
			expect(body.success).toBe(true);
		});

		it("after PATCH reorder, GET thread returns candidates in the new order", async () => {
			const thread = await createThreadViaAPI(app, "Order Verify");
			const c1 = await createCandidateViaAPI(app, thread.id, {
				name: "First",
				categoryId: 1,
			});
			const c2 = await createCandidateViaAPI(app, thread.id, {
				name: "Second",
				categoryId: 1,
			});
			const c3 = await createCandidateViaAPI(app, thread.id, {
				name: "Third",
				categoryId: 1,
			});

			// Reverse the order
			await app.request(`/api/threads/${thread.id}/candidates/reorder`, {
				method: "PATCH",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ orderedIds: [c3.id, c2.id, c1.id] }),
			});

			const res = await app.request(`/api/threads/${thread.id}`);
			expect(res.status).toBe(200);
			const body = await res.json();
			expect(body.candidates[0].id).toBe(c3.id);
			expect(body.candidates[1].id).toBe(c2.id);
			expect(body.candidates[2].id).toBe(c1.id);
		});

		it("on a resolved thread returns 400", async () => {
			const thread = await createThreadViaAPI(app, "Resolved Thread");
			const candidate = await createCandidateViaAPI(app, thread.id, {
				name: "Winner",
				categoryId: 1,
			});

			// Resolve the thread first
			await app.request(`/api/threads/${thread.id}/resolve`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ candidateId: candidate.id }),
			});

			const res = await app.request(
				`/api/threads/${thread.id}/candidates/reorder`,
				{
					method: "PATCH",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({ orderedIds: [candidate.id] }),
				},
			);

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

		it("with invalid body (empty orderedIds) returns 400", async () => {
			const thread = await createThreadViaAPI(app, "Invalid Body");

			const res = await app.request(
				`/api/threads/${thread.id}/candidates/reorder`,
				{
					method: "PATCH",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({ orderedIds: [] }),
				},
			);

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

		it("with missing orderedIds field returns 400", async () => {
			const thread = await createThreadViaAPI(app, "Missing Field");

			const res = await app.request(
				`/api/threads/${thread.id}/candidates/reorder`,
				{
					method: "PATCH",
					headers: { "Content-Type": "application/json" },
					body: JSON.stringify({}),
				},
			);

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

	describe("POST /api/threads/:id/resolve", () => {
		it("with valid candidateId returns 200 + created item", async () => {
			const thread = await createThreadViaAPI(app, "Tent Decision");
			const candidate = await createCandidateViaAPI(app, thread.id, {
				name: "Winner",
				categoryId: 1,
				priceCents: 30000,
			});

			const res = await app.request(`/api/threads/${thread.id}/resolve`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ candidateId: candidate.id }),
			});

			expect(res.status).toBe(200);
			const body = await res.json();
			expect(body.item).toBeDefined();
			expect(body.item.name).toBe("Winner");
			expect(body.item.priceCents).toBe(30000);
		});

		it("on already-resolved thread returns 400", async () => {
			const thread = await createThreadViaAPI(app, "Already Resolved");
			const candidate = await createCandidateViaAPI(app, thread.id, {
				name: "Winner",
				categoryId: 1,
			});

			// Resolve first time
			await app.request(`/api/threads/${thread.id}/resolve`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ candidateId: candidate.id }),
			});

			// Try again
			const res = await app.request(`/api/threads/${thread.id}/resolve`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ candidateId: candidate.id }),
			});

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

		it("with wrong candidateId returns 400", async () => {
			const t1 = await createThreadViaAPI(app, "Thread 1");
			const t2 = await createThreadViaAPI(app, "Thread 2");
			const candidate = await createCandidateViaAPI(app, t2.id, {
				name: "Wrong Thread",
				categoryId: 1,
			});

			const res = await app.request(`/api/threads/${t1.id}/resolve`, {
				method: "POST",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ candidateId: candidate.id }),
			});

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