/* @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: {
			findById: vi.fn(),
			findOne: vi.fn(),
			findByIdAndDelete: vi.fn(),
		},
		USER_ROLES,
	};
});

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

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

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

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

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

describe("PATCH /api/admin/users/[userId]", () => {
	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 PATCH(createRequestStub({}), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
		});

		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 PATCH(createRequestStub({ email: "x@y.de" }), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
		});

		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 PATCH(req, {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
		});

		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 PATCH(createRequestStub("nope"), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
		});

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

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

		const res = await PATCH(createRequestStub({ email: "x@y.de" }), {
			params: Promise.resolve({ userId: undefined }),
		});

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Missing required route parameter(s)",
				code: "VALIDATION_MISSING_PARAM",
				details: { params: ["userId"] },
			},
		});
	});

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

		const res = await PATCH(createRequestStub({ email: "x@y.de" }), {
			params: Promise.resolve({ userId: "nope" }),
		});

		expect(res.status).toBe(400);
		expect(await res.json()).toMatchObject({
			error: { code: "VALIDATION_INVALID_FIELD" },
		});
	});

	it("returns 404 when user does not exist", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		User.findById.mockReturnValue({
			exec: vi.fn().mockResolvedValue(null),
		});

		const res = await PATCH(createRequestStub({ email: "x@y.de" }), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
		});

		expect(res.status).toBe(404);
		expect(await res.json()).toEqual({
			error: {
				message: "Not found",
				code: "USER_NOT_FOUND",
				details: { userId: "507f1f77bcf86cd799439011" },
			},
		});
	});

	it("returns 400 when switching to role=branch without branchId (existing has none)", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "dev",
			branchId: null,
			email: "dev@example.com",
		});

		const user = {
			_id: "507f1f77bcf86cd799439011",
			username: "x",
			email: "x@example.com",
			role: "admin",
			branchId: null,
			mustChangePassword: false,
			createdAt: new Date(),
			updatedAt: new Date(),
			save: vi.fn(),
		};

		User.findById.mockReturnValue({
			exec: vi.fn().mockResolvedValue(user),
		});

		const res = await PATCH(createRequestStub({ role: "branch" }), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
		});

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Missing required fields",
				code: "VALIDATION_MISSING_FIELD",
				details: { fields: ["branchId"] },
			},
		});
	});

	it("returns 200 and updates fields; clears branchId for non-branch roles", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const user = {
			_id: "507f1f77bcf86cd799439011",
			username: "olduser",
			email: "old@example.com",
			role: "branch",
			branchId: "NL01",
			mustChangePassword: true,
			createdAt: new Date("2026-02-01T10:00:00.000Z"),
			updatedAt: new Date("2026-02-02T10:00:00.000Z"),
			save: vi.fn().mockResolvedValue(true),
		};

		User.findById.mockReturnValue({
			exec: vi.fn().mockResolvedValue(user),
		});

		const res = await PATCH(
			createRequestStub({
				role: "admin",
				mustChangePassword: false,
			}),
			{
				params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
			},
		);

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

		expect(user.role).toBe("admin");
		expect(user.branchId).toBe(null);
		expect(user.mustChangePassword).toBe(false);
		expect(user.save).toHaveBeenCalledTimes(1);

		const body = await res.json();
		expect(body).toMatchObject({
			ok: true,
			user: {
				id: "507f1f77bcf86cd799439011",
				username: "olduser",
				email: "old@example.com",
				role: "admin",
				branchId: null,
				mustChangePassword: false,
			},
		});
	});

	it("returns 400 when username is already taken by another user", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "dev",
			branchId: null,
			email: "dev@example.com",
		});

		const user = {
			_id: "507f1f77bcf86cd799439011",
			username: "olduser",
			email: "old@example.com",
			role: "admin",
			branchId: null,
			mustChangePassword: false,
			createdAt: new Date(),
			updatedAt: new Date(),
			save: vi.fn(),
		};

		User.findById.mockReturnValue({
			exec: vi.fn().mockResolvedValue(user),
		});

		User.findOne.mockReturnValue({
			select: vi.fn().mockReturnThis(),
			exec: vi.fn().mockResolvedValue({ _id: "507f1f77bcf86cd799439099" }),
		});

		const res = await PATCH(createRequestStub({ username: "TakenUser" }), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
		});

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

		const body = await res.json();
		expect(body).toEqual({
			error: {
				message: "Username already exists",
				code: "VALIDATION_INVALID_FIELD",
				details: { field: "username", value: "takenuser" },
			},
		});
	});
});

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

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

		const res = await POST(new Request("http://localhost/api/admin/users/x"), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
		});

		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(new Request("http://localhost/api/admin/users/x"), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
		});

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

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

		const res = await POST(new Request("http://localhost/api/admin/users/x"), {
			params: Promise.resolve({ userId: "nope" }),
		});

		expect(res.status).toBe(400);
		expect(await res.json()).toMatchObject({
			error: { code: "VALIDATION_INVALID_FIELD" },
		});
	});

	it("returns 400 when trying to reset the current user password", async () => {
		getSession.mockResolvedValue({
			userId: "507f1f77bcf86cd799439011",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const res = await POST(new Request("http://localhost/api/admin/users/x"), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
		});

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Cannot reset current user password",
				code: "VALIDATION_INVALID_FIELD",
				details: { field: "userId", reason: "SELF_PASSWORD_RESET_FORBIDDEN" },
			},
		});
	});

	it("returns 404 when user does not exist", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "dev",
			branchId: null,
			email: "dev@example.com",
		});

		User.findById.mockReturnValue({
			exec: vi.fn().mockResolvedValue(null),
		});

		const res = await POST(new Request("http://localhost/api/admin/users/x"), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439099" }),
		});

		expect(res.status).toBe(404);
		expect(await res.json()).toEqual({
			error: {
				message: "Not found",
				code: "USER_NOT_FOUND",
				details: { userId: "507f1f77bcf86cd799439099" },
			},
		});
	});

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

		const user = {
			_id: "507f1f77bcf86cd799439099",
			username: "branch2",
			email: "branch2@example.com",
			role: "branch",
			branchId: "NL02",
			passwordHash: "old-hash",
			mustChangePassword: false,
			passwordResetToken: "token",
			passwordResetExpiresAt: new Date("2030-01-01"),
			createdAt: new Date("2026-02-01T10:00:00.000Z"),
			updatedAt: new Date("2026-02-02T10:00:00.000Z"),
			save: vi.fn().mockResolvedValue(true),
		};

		User.findById.mockReturnValue({
			exec: vi.fn().mockResolvedValue(user),
		});

		generateAdminTemporaryPassword.mockReturnValue("TempPass123!");
		bcryptHash.mockResolvedValue("hashed-temp");

		const res = await POST(new Request("http://localhost/api/admin/users/x"), {
			params: Promise.resolve({ userId: "507f1f77bcf86cd799439099" }),
		});

		expect(res.status).toBe(200);
		expect(generateAdminTemporaryPassword).toHaveBeenCalledTimes(1);
		expect(bcryptHash).toHaveBeenCalledWith("TempPass123!", 12);
		expect(user.passwordHash).toBe("hashed-temp");
		expect(user.mustChangePassword).toBe(true);
		expect(user.passwordResetToken).toBe(null);
		expect(user.passwordResetExpiresAt).toBe(null);
		expect(user.save).toHaveBeenCalledTimes(1);

		expect(await res.json()).toMatchObject({
			ok: true,
			temporaryPassword: "TempPass123!",
			user: {
				id: "507f1f77bcf86cd799439099",
				username: "branch2",
				email: "branch2@example.com",
				role: "branch",
				branchId: "NL02",
				mustChangePassword: true,
			},
		});
	});
});

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

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

		const res = await DELETE(
			new Request("http://localhost/api/admin/users/x"),
			{
				params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
			},
		);

		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 DELETE(
			new Request("http://localhost/api/admin/users/x"),
			{
				params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
			},
		);

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

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

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

		const res = await DELETE(
			new Request("http://localhost/api/admin/users/x"),
			{
				params: Promise.resolve({ userId: "nope" }),
			},
		);

		expect(res.status).toBe(400);
		expect(await res.json()).toMatchObject({
			error: { code: "VALIDATION_INVALID_FIELD" },
		});
	});

	it("returns 400 when trying to delete the current user (self delete)", async () => {
		getSession.mockResolvedValue({
			userId: "507f1f77bcf86cd799439011",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		const res = await DELETE(
			new Request("http://localhost/api/admin/users/x"),
			{
				params: Promise.resolve({ userId: "507f1f77bcf86cd799439011" }),
			},
		);

		expect(res.status).toBe(400);
		expect(await res.json()).toEqual({
			error: {
				message: "Cannot delete current user",
				code: "VALIDATION_INVALID_FIELD",
				details: { field: "userId", reason: "SELF_DELETE_FORBIDDEN" },
			},
		});

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

	it("returns 404 when user does not exist", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "dev",
			branchId: null,
			email: "dev@example.com",
		});

		User.findByIdAndDelete.mockReturnValue({
			select: vi.fn().mockReturnThis(),
			exec: vi.fn().mockResolvedValue(null),
		});

		const res = await DELETE(
			new Request("http://localhost/api/admin/users/x"),
			{
				params: Promise.resolve({ userId: "507f1f77bcf86cd799439099" }),
			},
		);

		expect(res.status).toBe(404);
		expect(await res.json()).toEqual({
			error: {
				message: "Not found",
				code: "USER_NOT_FOUND",
				details: { userId: "507f1f77bcf86cd799439099" },
			},
		});
	});

	it("returns 200 and deleted user payload on success", async () => {
		getSession.mockResolvedValue({
			userId: "u2",
			role: "superadmin",
			branchId: null,
			email: "superadmin@example.com",
		});

		User.findByIdAndDelete.mockReturnValue({
			select: vi.fn().mockReturnThis(),
			exec: vi.fn().mockResolvedValue({
				_id: "507f1f77bcf86cd799439099",
				username: "todelete",
				email: "todelete@example.com",
				role: "branch",
				branchId: "NL01",
				mustChangePassword: true,
				createdAt: new Date("2026-02-01T10:00:00.000Z"),
				updatedAt: new Date("2026-02-02T10:00:00.000Z"),
			}),
		});

		const res = await DELETE(
			new Request("http://localhost/api/admin/users/x"),
			{
				params: Promise.resolve({ userId: "507f1f77bcf86cd799439099" }),
			},
		);

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

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