/* @vitest-environment node */

import { describe, it, expect, vi, beforeEach } from "vitest";

vi.mock("@/lib/auth/session", () => ({
	getSession: vi.fn(),
}));

vi.mock("@/lib/db", () => ({
	getDb: vi.fn(),
}));

vi.mock("@/models/user", () => {
	const USER_ROLES = Object.freeze({
		BRANCH: "branch",
		ADMIN: "admin",
		SUPERADMIN: "superadmin",
		DEV: "dev",
	});

	return {
		default: {
			find: vi.fn(),
			findOne: vi.fn(),
			create: vi.fn(),
		},
		USER_ROLES,
	};
});

vi.mock("bcryptjs", () => {
	const hash = vi.fn();
	return {
		default: { hash },
		hash,
	};
});

import { getSession } from "@/lib/auth/session";
import { getDb } from "@/lib/db";
import User from "@/models/user";
import { hash as bcryptHash } from "bcryptjs";

import { GET, POST, dynamic } from "./route.js";

function buildCursor(lastId) {
	return Buffer.from(JSON.stringify({ v: 1, lastId }), "utf8").toString(
		"base64url",
	);
}

function buildOffsetCursor(sort, offset) {
	return Buffer.from(JSON.stringify({ v: 2, sort, offset }), "utf8").toString(
		"base64url",
	);
}

function createRequestStub(body) {
	return {
		async json() {
			return body;
		},
	};
}

describe("GET /api/admin/users", () => {
	beforeEach(() => {
		vi.clearAllMocks();
		getDb.mockResolvedValue({});
	});

	it('exports dynamic="force-dynamic"', () => {
		expect(dynamic).toBe("force-dynamic");
	});

	it("returns 401 when unauthenticated", async () => {
		getSession.mockResolvedValue(null);

		const res = await GET(new Request("http://localhost/api/admin/users"));
		expect(res.status).toBe(401);

		expect(await res.json()).toEqual({
			error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" },
		});
	});

	it("returns 403 when authenticated but not allowed (admin)", async () => {
		getSession.mockResolvedValue({
			userId: "u1",
			role: "admin",
			branchId: null,
			email: "admin@example.com",
		});

		const res = await GET(new Request("http://localhost/api/admin/users"));
		expect(res.status).toBe(403);

		expect(await res.json()).toEqual({
			error: {
				message: "Forbidden",
				code: "AUTH_FORBIDDEN_USER_MANAGEMENT",
			},
		});

		expect(User.find).not.toHaveBeenCalled();
	});

	it("returns 200 with items and nextCursor (superadmin, limit + cursor)", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const d1 = {
			_id: "507f1f77bcf86cd799439013",
			username: "u3",
			email: "u3@example.com",
			role: "admin",
			branchId: null,
			mustChangePassword: false,
			createdAt: new Date("2026-02-01T10:00:00.000Z"),
			updatedAt: new Date("2026-02-02T10:00:00.000Z"),
		};
		const d2 = {
			_id: "507f1f77bcf86cd799439012",
			username: "u2",
			email: "u2@example.com",
			role: "branch",
			branchId: "NL01",
			mustChangePassword: true,
			createdAt: new Date("2026-02-01T09:00:00.000Z"),
			updatedAt: new Date("2026-02-02T09:00:00.000Z"),
		};
		const d3 = {
			_id: "507f1f77bcf86cd799439011",
			username: "u1",
			email: "u1@example.com",
			role: "dev",
			branchId: null,
			mustChangePassword: false,
			createdAt: new Date("2026-02-01T08:00:00.000Z"),
			updatedAt: new Date("2026-02-02T08:00:00.000Z"),
		};

		const chain = {
			sort: vi.fn().mockReturnThis(),
			limit: vi.fn().mockReturnThis(),
			select: vi.fn().mockReturnThis(),
			exec: vi.fn().mockResolvedValue([d1, d2, d3]),
		};

		User.find.mockReturnValue(chain);

		const res = await GET(
			new Request("http://localhost/api/admin/users?limit=2"),
		);

		expect(res.status).toBe(200);

		expect(chain.sort).toHaveBeenCalledWith({ _id: -1 });
		expect(chain.limit).toHaveBeenCalledWith(3); // limit + 1

		const body = await res.json();

		expect(body.items).toHaveLength(2);
		expect(body.items[0]).toMatchObject({
			id: "507f1f77bcf86cd799439013",
			username: "u3",
			email: "u3@example.com",
			role: "admin",
			branchId: null,
			mustChangePassword: false,
		});
		expect(body.items[1]).toMatchObject({
			id: "507f1f77bcf86cd799439012",
			username: "u2",
			email: "u2@example.com",
			role: "branch",
			branchId: "NL01",
			mustChangePassword: true,
		});

		expect(body.nextCursor).toBe(buildCursor("507f1f77bcf86cd799439012"));
	});

	it("returns 400 for invalid sort parameter", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const res = await GET(
			new Request("http://localhost/api/admin/users?sort=unknown"),
		);

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Invalid sort",
				code: "VALIDATION_INVALID_FIELD",
				details: {
					field: "sort",
					value: "unknown",
					allowed: ["default", "role_rights", "branch_asc"],
				},
			},
		});
	});

	it("returns sorted users for sort=role_rights with stable nextCursor", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const docs = [
			{
				_id: "507f1f77bcf86cd799439011",
				username: "branchb",
				email: "branchb@example.com",
				role: "branch",
				branchId: "NL10",
				mustChangePassword: false,
			},
			{
				_id: "507f1f77bcf86cd799439012",
				username: "admin",
				email: "admin@example.com",
				role: "admin",
				branchId: null,
				mustChangePassword: false,
			},
			{
				_id: "507f1f77bcf86cd799439013",
				username: "dev",
				email: "dev@example.com",
				role: "dev",
				branchId: null,
				mustChangePassword: false,
			},
			{
				_id: "507f1f77bcf86cd799439014",
				username: "super",
				email: "super@example.com",
				role: "superadmin",
				branchId: null,
				mustChangePassword: false,
			},
		];

		const chain = {
			select: vi.fn().mockReturnThis(),
			exec: vi.fn().mockResolvedValue(docs),
		};

		User.find.mockReturnValue(chain);

		const res = await GET(
			new Request("http://localhost/api/admin/users?sort=role_rights&limit=2"),
		);

		expect(res.status).toBe(200);
		expect(chain.select).toHaveBeenCalledTimes(1);
		expect(chain.exec).toHaveBeenCalledTimes(1);

		const body = await res.json();
		expect(body.items.map((x) => x.role)).toEqual(["superadmin", "dev"]);
		expect(body.nextCursor).toBe(buildOffsetCursor("role_rights", 2));
	});

	it("returns sorted users for sort=branch_asc with null branches at the end", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const docs = [
			{
				_id: "507f1f77bcf86cd799439011",
				username: "branch10",
				email: "branch10@example.com",
				role: "branch",
				branchId: "NL10",
				mustChangePassword: false,
			},
			{
				_id: "507f1f77bcf86cd799439012",
				username: "admin",
				email: "admin@example.com",
				role: "admin",
				branchId: null,
				mustChangePassword: false,
			},
			{
				_id: "507f1f77bcf86cd799439013",
				username: "branch2",
				email: "branch2@example.com",
				role: "branch",
				branchId: "NL2",
				mustChangePassword: false,
			},
			{
				_id: "507f1f77bcf86cd799439014",
				username: "branch1",
				email: "branch1@example.com",
				role: "branch",
				branchId: "NL01",
				mustChangePassword: false,
			},
		];

		const chain = {
			select: vi.fn().mockReturnThis(),
			exec: vi.fn().mockResolvedValue(docs),
		};

		User.find.mockReturnValue(chain);

		const res = await GET(
			new Request("http://localhost/api/admin/users?sort=branch_asc&limit=10"),
		);

		expect(res.status).toBe(200);
		const body = await res.json();
		expect(body.items.map((x) => x.branchId)).toEqual([
			"NL01",
			"NL2",
			"NL10",
			null,
		]);
		expect(body.nextCursor).toBe(null);
	});

	it("returns 400 when cursor sort context does not match sort parameter", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const cursor = buildOffsetCursor("role_rights", 2);
		const res = await GET(
			new Request(
				`http://localhost/api/admin/users?sort=branch_asc&cursor=${encodeURIComponent(cursor)}`,
			),
		);

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Invalid cursor",
				code: "VALIDATION_INVALID_FIELD",
				details: { field: "cursor" },
			},
		});
	});

	it("keeps pagination stable within the same custom sort mode", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const docs = [
			{
				_id: "507f1f77bcf86cd799439011",
				username: "branchb",
				email: "branchb@example.com",
				role: "branch",
				branchId: "NL10",
				mustChangePassword: false,
			},
			{
				_id: "507f1f77bcf86cd799439012",
				username: "admin",
				email: "admin@example.com",
				role: "admin",
				branchId: null,
				mustChangePassword: false,
			},
			{
				_id: "507f1f77bcf86cd799439013",
				username: "dev",
				email: "dev@example.com",
				role: "dev",
				branchId: null,
				mustChangePassword: false,
			},
			{
				_id: "507f1f77bcf86cd799439014",
				username: "super",
				email: "super@example.com",
				role: "superadmin",
				branchId: null,
				mustChangePassword: false,
			},
		];

		const chain = {
			select: vi.fn().mockReturnThis(),
			exec: vi.fn().mockResolvedValue(docs),
		};

		User.find.mockReturnValue(chain);

		const firstRes = await GET(
			new Request("http://localhost/api/admin/users?sort=role_rights&limit=2"),
		);
		expect(firstRes.status).toBe(200);

		const firstBody = await firstRes.json();
		expect(firstBody.items.map((x) => x.role)).toEqual(["superadmin", "dev"]);
		expect(firstBody.nextCursor).toBe(buildOffsetCursor("role_rights", 2));

		const secondRes = await GET(
			new Request(
				`http://localhost/api/admin/users?sort=role_rights&limit=2&cursor=${encodeURIComponent(firstBody.nextCursor)}`,
			),
		);
		expect(secondRes.status).toBe(200);

		const secondBody = await secondRes.json();
		expect(secondBody.items.map((x) => x.role)).toEqual(["admin", "branch"]);
		expect(secondBody.nextCursor).toBe(null);
	});
});

describe("POST /api/admin/users", () => {
	beforeEach(() => {
		vi.clearAllMocks();
		getDb.mockResolvedValue({});
		bcryptHash.mockResolvedValue("hashed");
	});

	it("returns 401 when unauthenticated", async () => {
		getSession.mockResolvedValue(null);

		const res = await POST(createRequestStub({}));
		expect(res.status).toBe(401);

		expect(await res.json()).toEqual({
			error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" },
		});
	});

	it("returns 403 when authenticated but not allowed (admin)", async () => {
		getSession.mockResolvedValue({
			userId: "u1",
			role: "admin",
			branchId: null,
			email: "admin@example.com",
		});

		const res = await POST(createRequestStub({}));
		expect(res.status).toBe(403);

		expect(await res.json()).toEqual({
			error: {
				message: "Forbidden",
				code: "AUTH_FORBIDDEN_USER_MANAGEMENT",
			},
		});
	});

	it("returns 400 when JSON parsing fails", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const req = { json: vi.fn().mockRejectedValue(new Error("invalid json")) };

		const res = await POST(req);
		expect(res.status).toBe(400);

		expect(await res.json()).toEqual({
			error: {
				message: "Invalid request body",
				code: "VALIDATION_INVALID_JSON",
			},
		});
	});

	it("returns 400 when body is not an object", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const res = await POST(createRequestStub("nope"));
		expect(res.status).toBe(400);

		expect(await res.json()).toEqual({
			error: {
				message: "Invalid request body",
				code: "VALIDATION_INVALID_BODY",
			},
		});
	});

	it("returns 400 when fields are missing", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "dev",
			branchId: null,
			email: "dev@example.com",
		});

		const res = await POST(createRequestStub({}));
		expect(res.status).toBe(400);

		expect(await res.json()).toEqual({
			error: {
				message: "Missing required fields",
				code: "VALIDATION_MISSING_FIELD",
				details: { fields: ["username", "email", "role", "initialPassword"] },
			},
		});
	});

	it("returns 400 for invalid role", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const res = await POST(
			createRequestStub({
				username: "newuser",
				email: "new@example.com",
				role: "nope",
				initialPassword: "StrongPassword123",
			}),
		);

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

		const body = await res.json();
		expect(body.error.code).toBe("VALIDATION_INVALID_FIELD");
		expect(body.error.details.field).toBe("role");
	});

	it("returns 400 for invalid branchId when role=branch", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const res = await POST(
			createRequestStub({
				username: "newuser",
				email: "new@example.com",
				role: "branch",
				branchId: "XX1",
				initialPassword: "StrongPassword123",
			}),
		);

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

		const body = await res.json();
		expect(body.error.code).toBe("VALIDATION_INVALID_FIELD");
		expect(body.error.details.field).toBe("branchId");
	});

	it("returns 400 for weak initialPassword", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "dev",
			branchId: null,
			email: "dev@example.com",
		});

		const res = await POST(
			createRequestStub({
				username: "newuser",
				email: "new@example.com",
				role: "admin",
				initialPassword: "short1",
			}),
		);

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

		const body = await res.json();
		expect(body.error.code).toBe("VALIDATION_WEAK_PASSWORD");
		expect(body.error.details).toMatchObject({
			minLength: 8,
			requireLetter: true,
			requireNumber: true,
		});
		expect(Array.isArray(body.error.details.reasons)).toBe(true);

		expect(User.create).not.toHaveBeenCalled();
	});

	it("returns 400 when username already exists", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		User.findOne.mockImplementation((query) => {
			if (query?.username) {
				return {
					select: vi.fn().mockReturnThis(),
					exec: vi.fn().mockResolvedValue({ _id: "x" }),
				};
			}
			if (query?.email) {
				return {
					select: vi.fn().mockReturnThis(),
					exec: vi.fn().mockResolvedValue(null),
				};
			}
			return {
				select: vi.fn().mockReturnThis(),
				exec: vi.fn().mockResolvedValue(null),
			};
		});

		const res = await POST(
			createRequestStub({
				username: "NewUser",
				email: "new@example.com",
				role: "admin",
				initialPassword: "StrongPassword123",
			}),
		);

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Username already exists",
				code: "VALIDATION_INVALID_FIELD",
				details: { field: "username" },
			},
		});

		expect(User.create).not.toHaveBeenCalled();
		expect(bcryptHash).not.toHaveBeenCalled();
	});

	it("returns 400 when email already exists", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		User.findOne.mockImplementation((query) => {
			if (query?.username) {
				return {
					select: vi.fn().mockReturnThis(),
					exec: vi.fn().mockResolvedValue(null),
				};
			}
			if (query?.email) {
				return {
					select: vi.fn().mockReturnThis(),
					exec: vi.fn().mockResolvedValue({ _id: "y" }),
				};
			}
			return {
				select: vi.fn().mockReturnThis(),
				exec: vi.fn().mockResolvedValue(null),
			};
		});

		const res = await POST(
			createRequestStub({
				username: "newuser",
				email: "NEW@EXAMPLE.COM",
				role: "admin",
				initialPassword: "StrongPassword123",
			}),
		);

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Email already exists",
				code: "VALIDATION_INVALID_FIELD",
				details: { field: "email" },
			},
		});

		expect(User.create).not.toHaveBeenCalled();
		expect(bcryptHash).not.toHaveBeenCalled();
	});

	it("returns 400 when username AND email already exist", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		User.findOne.mockImplementation((query) => {
			if (query?.username) {
				return {
					select: vi.fn().mockReturnThis(),
					exec: vi.fn().mockResolvedValue({ _id: "x" }),
				};
			}
			if (query?.email) {
				return {
					select: vi.fn().mockReturnThis(),
					exec: vi.fn().mockResolvedValue({ _id: "y" }),
				};
			}
			return {
				select: vi.fn().mockReturnThis(),
				exec: vi.fn().mockResolvedValue(null),
			};
		});

		const res = await POST(
			createRequestStub({
				username: "newuser",
				email: "new@example.com",
				role: "admin",
				initialPassword: "StrongPassword123",
			}),
		);

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Username and email already exist",
				code: "VALIDATION_INVALID_FIELD",
				details: { fields: ["username", "email"] },
			},
		});

		expect(User.create).not.toHaveBeenCalled();
		expect(bcryptHash).not.toHaveBeenCalled();
	});

	it("returns 200 and creates user with hashed password + mustChangePassword=true", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		// No duplicates
		User.findOne.mockImplementation(() => {
			return {
				select: vi.fn().mockReturnThis(),
				exec: vi.fn().mockResolvedValue(null),
			};
		});

		User.create.mockResolvedValue({
			_id: "507f1f77bcf86cd799439099",
			username: "newuser",
			email: "new@example.com",
			role: "branch",
			branchId: "NL01",
			mustChangePassword: true,
			createdAt: new Date("2026-02-06T10:00:00.000Z"),
			updatedAt: new Date("2026-02-06T10:00:00.000Z"),
		});

		const res = await POST(
			createRequestStub({
				username: "NewUser",
				email: "NEW@EXAMPLE.COM",
				role: "branch",
				branchId: "nl01",
				initialPassword: "StrongPassword123",
			}),
		);

		expect(res.status).toBe(200);

		expect(getDb).toHaveBeenCalledTimes(1);
		expect(bcryptHash).toHaveBeenCalledWith("StrongPassword123", 12);

		expect(User.findOne).toHaveBeenCalledWith({ username: "newuser" });
		expect(User.findOne).toHaveBeenCalledWith({ email: "new@example.com" });

		expect(User.create).toHaveBeenCalledWith(
			expect.objectContaining({
				username: "newuser",
				email: "new@example.com",
				role: "branch",
				branchId: "NL01",
				passwordHash: "hashed",
				mustChangePassword: true,
			}),
		);

		const body = await res.json();
		expect(body).toMatchObject({
			ok: true,
			user: {
				id: "507f1f77bcf86cd799439099",
				username: "newuser",
				email: "new@example.com",
				role: "branch",
				branchId: "NL01",
				mustChangePassword: true,
			},
		});
	});
});
