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

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

import { load, actions } from './+page.server';
import type { UserGroup } from './+page.server';

// PageServerLoad annotates the return as `void | (...)`. This explicit shape avoids
// the void and the Record<string, any> from the generic constraint.
type LoadData = {
	invites: unknown[];
	status: string;
	loadError: string | null;
	groups: UserGroup[];
	groupsLoadError: string | null;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyFetch = (...args: any[]) => any;

function mockResponse(ok: boolean, body: unknown, status = 200) {
	return {
		ok,
		status,
		json: async () => body,
		text: async () => JSON.stringify(body),
		headers: new Headers({ 'content-type': 'application/json' })
	} as unknown as Response;
}

describe('admin/invites load()', () => {
	const mockFetch = vi.fn<AnyFetch>();

	beforeEach(() => mockFetch.mockReset());

	function event(status = 'active') {
		const url = new URL(`http://localhost/admin/invites?status=${status}`);
		return {
			url,
			request: new Request(url),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any;
	}

	it('returns groups array alongside invites when both succeed', async () => {
		mockFetch.mockResolvedValueOnce(mockResponse(true, [])).mockResolvedValueOnce(
			mockResponse(true, [
				{ id: 'g-1', name: 'Familie', permissions: ['READ_ALL'] },
				{ id: 'g-2', name: 'Administratoren', permissions: ['ADMIN'] }
			])
		);

		const result = (await load(event())) as LoadData;

		expect(result.groups).toHaveLength(2);
		expect(result.groupsLoadError).toBeNull();
	});

	it('returns groups sorted alphabetically by name', async () => {
		mockFetch.mockResolvedValueOnce(mockResponse(true, [])).mockResolvedValueOnce(
			mockResponse(true, [
				{ id: 'g-1', name: 'Zebra', permissions: [] },
				{ id: 'g-2', name: 'Alfa', permissions: [] },
				{ id: 'g-3', name: 'Mitte', permissions: [] }
			])
		);

		const result = (await load(event())) as LoadData;

		expect(result.groups.map((g) => g.name)).toEqual(['Alfa', 'Mitte', 'Zebra']);
	});

	it('returns groups: [] and non-null groupsLoadError when groups fetch is non-OK', async () => {
		mockFetch
			.mockResolvedValueOnce(mockResponse(true, []))
			.mockResolvedValueOnce(mockResponse(false, { code: 'FORBIDDEN' }, 403));

		const result = (await load(event())) as LoadData;

		expect(result.groups).toEqual([]);
		expect(result.groupsLoadError).toBe('FORBIDDEN');
	});

	it('falls back to INTERNAL_ERROR when groups error body has no code', async () => {
		mockFetch
			.mockResolvedValueOnce(mockResponse(true, []))
			.mockResolvedValueOnce(mockResponse(false, null, 500));

		const result = (await load(event())) as LoadData;

		expect(result.groupsLoadError).toBe('INTERNAL_ERROR');
	});

	it('fetches invites and groups in parallel (both URLs called)', async () => {
		mockFetch
			.mockResolvedValueOnce(mockResponse(true, []))
			.mockResolvedValueOnce(mockResponse(true, []));

		await load(event());

		expect(mockFetch).toHaveBeenCalledTimes(2);
		// createApiClient calls fetch(Request, {}), not fetch(string, init)
		const urls = mockFetch.mock.calls.map((call) => (call[0] as Request).url);
		expect(urls).toEqual(
			expect.arrayContaining([
				expect.stringContaining('/api/invites'),
				expect.stringContaining('/api/groups')
			])
		);
	});
});

describe('admin/invites create action', () => {
	const mockFetch = vi.fn<AnyFetch>();

	beforeEach(() => mockFetch.mockReset());

	const successBody = {
		id: 'inv-1',
		code: 'ABCDE12345',
		displayCode: 'ABCDE-12345',
		status: 'active',
		revoked: false,
		useCount: 0,
		createdAt: '2026-01-01T00:00:00Z',
		shareableUrl: 'http://localhost/register?code=ABCDE12345'
	};

	it('includes groupIds array in POST body when checkboxes are checked', async () => {
		const fd = new FormData();
		fd.append('groupIds', 'g-1');
		fd.append('groupIds', 'g-2');
		mockFetch.mockResolvedValueOnce(mockResponse(true, successBody, 201));

		await actions.create({
			request: new Request('http://localhost', { method: 'POST', body: fd }),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any);

		// createApiClient calls fetch(Request, {}), not fetch(string, init)
		const [req] = mockFetch.mock.calls[0] as [Request, unknown];
		expect(req).toBeInstanceOf(Request);
		expect(req.url).toContain('/api/invites');
		const sent = await req.json();
		expect(sent.groupIds).toEqual(['g-1', 'g-2']);
	});

	it('sends groupIds: [] when no checkboxes are checked', async () => {
		const fd = new FormData();
		mockFetch.mockResolvedValueOnce(mockResponse(true, successBody, 201));

		await actions.create({
			request: new Request('http://localhost', { method: 'POST', body: fd }),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any);

		const [req] = mockFetch.mock.calls[0] as [Request, unknown];
		expect(req).toBeInstanceOf(Request);
		const sent = await req.json();
		expect(sent.groupIds).toEqual([]);
	});

	it('returns created invite on success', async () => {
		const fd = new FormData();
		mockFetch.mockResolvedValueOnce(mockResponse(true, successBody, 201));

		const result = await actions.create({
			request: new Request('http://localhost', { method: 'POST', body: fd }),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any);

		expect(result).toMatchObject({ created: expect.objectContaining({ id: 'inv-1' }) });
	});

	it('returns fail with backend error code when create returns non-OK', async () => {
		const fd = new FormData();
		mockFetch.mockResolvedValueOnce(mockResponse(false, { code: 'FORBIDDEN' }, 403));

		const result = await actions.create({
			request: new Request('http://localhost', { method: 'POST', body: fd }),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any);

		expect(result).toMatchObject({ status: 403, data: { createError: 'FORBIDDEN' } });
	});

	it('falls back to INTERNAL_ERROR when create error body has no code', async () => {
		const fd = new FormData();
		mockFetch.mockResolvedValueOnce(mockResponse(false, null, 500));

		const result = await actions.create({
			request: new Request('http://localhost', { method: 'POST', body: fd }),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any);

		expect(result).toMatchObject({ status: 500, data: { createError: 'INTERNAL_ERROR' } });
	});

	it('includes expiresAt in POST body when provided', async () => {
		const fd = new FormData();
		fd.append('expiresAt', '2026-12-31');
		mockFetch.mockResolvedValueOnce(mockResponse(true, successBody, 201));

		await actions.create({
			request: new Request('http://localhost', { method: 'POST', body: fd }),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any);

		const [req] = mockFetch.mock.calls[0] as [Request, unknown];
		const sent = await req.json();
		expect(sent.expiresAt).toBe('2026-12-31');
	});
});

describe('admin/invites revoke action', () => {
	const mockFetch = vi.fn<AnyFetch>();

	beforeEach(() => mockFetch.mockReset());

	it('calls DELETE /api/invites/{id} via createApiClient', async () => {
		const fd = new FormData();
		fd.append('id', 'inv-abc');
		mockFetch.mockResolvedValueOnce(mockResponse(true, null, 200));

		await actions.revoke({
			request: new Request('http://localhost', { method: 'POST', body: fd }),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any);

		const [req] = mockFetch.mock.calls[0] as [Request, unknown];
		expect(req).toBeInstanceOf(Request);
		expect(req.url).toContain('/api/invites/inv-abc');
		expect(req.method).toBe('DELETE');
	});

	it('returns revoked id on success', async () => {
		const fd = new FormData();
		fd.append('id', 'inv-abc');
		mockFetch.mockResolvedValueOnce(mockResponse(true, null, 200));

		const result = await actions.revoke({
			request: new Request('http://localhost', { method: 'POST', body: fd }),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any);

		expect(result).toEqual({ revoked: 'inv-abc' });
	});

	it('returns fail with backend error code when revoke returns non-OK', async () => {
		const fd = new FormData();
		fd.append('id', 'inv-abc');
		mockFetch.mockResolvedValueOnce(mockResponse(false, { code: 'NOT_FOUND' }, 404));

		const result = await actions.revoke({
			request: new Request('http://localhost', { method: 'POST', body: fd }),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any);

		expect(result).toMatchObject({ status: 404, data: { revokeError: 'NOT_FOUND' } });
	});

	it('returns fail(400) when revoke id is missing', async () => {
		const result = await actions.revoke({
			request: new Request('http://localhost', { method: 'POST', body: new FormData() }),
			fetch: mockFetch as unknown as typeof fetch
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
		} as any);

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