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

vi.mock('$env/dynamic/private', () => ({
	env: { API_INTERNAL_URL: 'http://localhost:8080' }
}));

vi.mock('$lib/shared/api.server', () => ({
	createApiClient: vi.fn(),
	extractErrorCode: (e: unknown) => (e as { code?: string } | undefined)?.code
}));

import { load, actions } from './+page.server';
import { createApiClient } from '$lib/shared/api.server';

function mockApi(methods: Partial<Record<'GET' | 'PUT' | 'DELETE', ReturnType<typeof vi.fn>>>) {
	vi.mocked(createApiClient).mockReturnValue(methods as ReturnType<typeof createApiClient>);
}

// ─── load() ──────────────────────────────────────────────────────────────────

describe('admin/users/[id] load()', () => {
	beforeEach(() => vi.clearAllMocks());

	function makeEvent(permissions: string[] = ['ADMIN']) {
		return {
			params: { id: 'user-123' },
			fetch: vi.fn() as unknown as typeof fetch,
			locals: { user: { groups: [{ permissions }] } },
			request: new Request('http://localhost/admin/users/user-123'),
			url: new URL('http://localhost/admin/users/user-123')
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any;
	}

	it('throws 403 when the user lacks the ADMIN permission', async () => {
		let thrown: unknown;
		try {
			await load(makeEvent(['READ_ALL']));
		} catch (e) {
			thrown = e;
		}

		expect((thrown as { status: number }).status).toBe(403);
	});

	it('throws 404 when the backend returns non-ok for the user lookup', async () => {
		const mockGet = vi
			.fn()
			.mockResolvedValueOnce({ response: { ok: false, status: 404 }, data: undefined })
			.mockResolvedValueOnce({ response: { ok: true }, data: [] });
		mockApi({ GET: mockGet });

		let thrown: unknown;
		try {
			await load(makeEvent());
		} catch (e) {
			thrown = e;
		}

		expect((thrown as { status: number }).status).toBe(404);
	});

	it('returns editUser and groups on success', async () => {
		const editUser = { id: 'user-123', email: 'max@example.com', firstName: 'Max' };
		const groups = [
			{ id: 'g-1', name: 'Familie', permissions: ['READ_ALL'] },
			{ id: 'g-2', name: 'Administratoren', permissions: ['ADMIN'] }
		];
		const mockGet = vi
			.fn()
			.mockResolvedValueOnce({ response: { ok: true }, data: editUser })
			.mockResolvedValueOnce({ response: { ok: true }, data: groups });
		mockApi({ GET: mockGet });

		const result = await load(makeEvent());

		expect(result).toMatchObject({
			editUser: expect.objectContaining({ id: 'user-123' }),
			groups: expect.arrayContaining([expect.objectContaining({ id: 'g-1' })])
		});
	});
});

// ─── update action ────────────────────────────────────────────────────────────

describe('admin/users/[id] update action', () => {
	beforeEach(() => vi.clearAllMocks());

	function makeUpdateRequest(fields: Record<string, string | string[]> = {}) {
		const fd = new FormData();
		const defaults: Record<string, string> = {
			firstName: 'Max',
			lastName: 'Mustermann',
			email: 'max@example.com'
		};
		for (const [k, v] of Object.entries({ ...defaults, ...fields })) {
			if (Array.isArray(v)) {
				v.forEach((item) => fd.append(k, item));
			} else {
				fd.append(k, v);
			}
		}
		return new Request('http://localhost', { method: 'POST', body: fd });
	}

	function makeEvent(request: Request) {
		return {
			params: { id: 'user-123' },
			request,
			fetch: vi.fn() as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any;
	}

	it('calls PUT /api/users/{id} and returns success: true on 200', async () => {
		const mockPut = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: {} });
		mockApi({ PUT: mockPut });

		const result = await actions.update(makeEvent(makeUpdateRequest()));

		expect(mockPut).toHaveBeenCalledWith(
			'/api/users/{id}',
			expect.objectContaining({ params: { path: { id: 'user-123' } } })
		);
		expect(result).toEqual({ success: true });
	});

	it('returns fail with backend error code when PUT returns non-OK', async () => {
		const mockPut = vi
			.fn()
			.mockResolvedValue({ response: { ok: false, status: 403 }, error: { code: 'FORBIDDEN' } });
		mockApi({ PUT: mockPut });

		const result = await actions.update(makeEvent(makeUpdateRequest()));

		expect(result).toMatchObject({ status: 403 });
	});

	it('returns fail with generic message when error body has no code field', async () => {
		const mockPut = vi
			.fn()
			.mockResolvedValue({ response: { ok: false, status: 500 }, error: null });
		mockApi({ PUT: mockPut });

		const result = await actions.update(makeEvent(makeUpdateRequest()));

		expect(result).toMatchObject({ status: 500 });
	});

	it('returns fail without calling backend when passwords do not match', async () => {
		const mockPut = vi.fn();
		mockApi({ PUT: mockPut });

		const result = await actions.update(
			makeEvent(makeUpdateRequest({ newPassword: 'abc', confirmPassword: 'xyz' }))
		);

		expect(mockPut).not.toHaveBeenCalled();
		expect(result).toMatchObject({ status: 400 });
	});
});

// ─── delete action ────────────────────────────────────────────────────────────

describe('admin/users/[id] delete action', () => {
	beforeEach(() => vi.clearAllMocks());

	function makeEvent() {
		return {
			params: { id: 'user-123' },
			fetch: vi.fn() as unknown as typeof fetch,
			request: new Request('http://localhost/admin/users/user-123')
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any;
	}

	it('redirects to /admin/users on successful delete', async () => {
		const mockDelete = vi.fn().mockResolvedValue({ response: { ok: true } });
		mockApi({ DELETE: mockDelete });

		let redirectLocation: string | null = null;
		try {
			await actions.delete(makeEvent());
		} catch (e: unknown) {
			const r = e as { location?: string };
			redirectLocation = r.location ?? null;
		}

		expect(redirectLocation).toBe('/admin/users');
	});

	it('returns fail when delete returns non-OK', async () => {
		const mockDelete = vi
			.fn()
			.mockResolvedValue({ response: { ok: false, status: 403 }, error: { code: 'FORBIDDEN' } });
		mockApi({ DELETE: mockDelete });

		const result = await actions.delete(makeEvent());

		expect(result).toMatchObject({ status: 403 });
	});
});
