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

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

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

const mockFetch = vi.fn() as unknown as typeof fetch;

// fail() returns a union type that TS won't narrow; read its data loosely.
const failData = (r: unknown) => (r as { data: Record<string, unknown> }).data;

beforeEach(() => vi.clearAllMocks());

function localsWith(perms: string[] | null) {
	if (perms === null) return { user: null };
	return { user: { groups: [{ permissions: perms }] } };
}

function loadEvent(perms: string[] | null, id = 'e1') {
	const url = new URL(`http://localhost/zeitstrahl/events/${id}/edit`);
	return {
		locals: localsWith(perms),
		url,
		params: { id },
		fetch: mockFetch,
		request: new Request(url),
		route: { id: '/zeitstrahl/events/[id]/edit' }
	} as never;
}

function actionEvent(
	method: 'save' | 'delete',
	fields: Record<string, string | string[]>,
	id = 'e1'
) {
	const fd = new FormData();
	for (const [k, v] of Object.entries(fields)) {
		if (Array.isArray(v)) v.forEach((x) => fd.append(k, x));
		else fd.set(k, v);
	}
	return {
		request: new Request(`http://localhost/zeitstrahl/events/${id}/edit?/${method}`, {
			method: 'POST',
			body: fd
		}),
		params: { id },
		fetch: mockFetch,
		route: { id: '/zeitstrahl/events/[id]/edit' }
	} as never;
}

const EVENT_VIEW = {
	id: 'e1',
	title: 'Umzug',
	type: 'PERSONAL',
	eventDate: '1925-04-01',
	precision: 'DAY',
	version: 2,
	createdBy: 'u1',
	createdAt: '2026-01-01T00:00:00Z',
	updatedBy: 'u1',
	updatedAt: '2026-01-01T00:00:00Z',
	persons: [],
	documents: []
};

describe('zeitstrahl/events/[id]/edit load — gating (REQ-002/003)', () => {
	it('throws 403 for an unauthenticated (null) user', async () => {
		await expect(load(loadEvent(null))).rejects.toMatchObject({ status: 403 });
	});

	it('throws 403 for a user without WRITE_ALL', async () => {
		await expect(load(loadEvent(['READ_ALL']))).rejects.toMatchObject({ status: 403 });
	});
});

describe('zeitstrahl/events/[id]/edit load — fail closed (REQ-012)', () => {
	it('throws 404 when the GET is not ok (unknown or derived id)', async () => {
		vi.mocked(createApiClient).mockReturnValue({
			GET: vi.fn().mockResolvedValue({ response: { ok: false, status: 404 }, data: undefined })
		} as never);
		await expect(load(loadEvent(['WRITE_ALL']))).rejects.toMatchObject({ status: 404 });
	});

	it('seeds the form with the event on an ok GET', async () => {
		vi.mocked(createApiClient).mockReturnValue({
			GET: vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: EVENT_VIEW })
		} as never);
		const result = await load(loadEvent(['WRITE_ALL']));
		expect(result.event).toMatchObject({ id: 'e1', title: 'Umzug' });
	});
});

describe('zeitstrahl/events/[id]/edit save action (REQ-005/013)', () => {
	it('updates via PUT (with version) and redirects on success', async () => {
		const put = vi
			.fn()
			.mockResolvedValue({ response: { ok: true, status: 200 }, data: EVENT_VIEW });
		vi.mocked(createApiClient).mockReturnValue({ PUT: put } as never);

		await expect(
			actions.save(
				actionEvent('save', {
					title: 'Umzug II',
					type: 'PERSONAL',
					eventDate: '1925-04-01',
					version: '2'
				})
			)
		).rejects.toMatchObject({ status: 303, location: '/zeitstrahl' });

		expect(put).toHaveBeenCalledTimes(1);
		expect(put.mock.calls[0][1].params.path.id).toBe('e1');
		expect(put.mock.calls[0][1].body).toMatchObject({ title: 'Umzug II', version: 2 });
	});

	it('maps a 409 conflict and does not redirect', async () => {
		const put = vi
			.fn()
			.mockResolvedValue({ response: { ok: false, status: 409 }, error: { code: 'CONFLICT' } });
		vi.mocked(createApiClient).mockReturnValue({ PUT: put } as never);

		const result = await actions.save(
			actionEvent('save', { title: 'Umzug', type: 'PERSONAL', eventDate: '1925-04-01' })
		);
		expect(result).toMatchObject({ status: 409 });
		expect(failData(result).error).toBeTruthy();
	});
});

describe('zeitstrahl/events/[id]/edit delete action (REQ-006/007)', () => {
	const validUuid = '22222222-2222-2222-2222-222222222222';

	it('deletes via DELETE and redirects to the resolved target on success', async () => {
		const del = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 } });
		vi.mocked(createApiClient).mockReturnValue({ DELETE: del } as never);

		await expect(
			actions.delete(actionEvent('delete', { originPersonId: validUuid }))
		).rejects.toMatchObject({ status: 303, location: `/persons/${validUuid}` });
		expect(del.mock.calls[0][1].params.path.id).toBe('e1');
	});

	it('returns fail(status) and does not redirect when DELETE is not ok', async () => {
		const del = vi
			.fn()
			.mockResolvedValue({ response: { ok: false, status: 500 }, error: { code: 'INTERNAL' } });
		vi.mocked(createApiClient).mockReturnValue({ DELETE: del } as never);

		const result = await actions.delete(actionEvent('delete', {}));
		expect(result).toMatchObject({ status: 500 });
		expect(failData(result).error).toBeTruthy();
	});
});
