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, search = '') {
	const url = new URL(`http://localhost/zeitstrahl/events/new${search}`);
	return {
		locals: localsWith(perms),
		url,
		fetch: mockFetch,
		request: new Request(url),
		route: { id: '/zeitstrahl/events/new' },
		params: {}
	} as never;
}

function saveRequest(fields: Record<string, string | string[]>): Request {
	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 new Request('http://localhost/zeitstrahl/events/new', { method: 'POST', body: fd });
}

function saveEvent(fields: Record<string, string | string[]>) {
	return {
		request: saveRequest(fields),
		fetch: mockFetch,
		route: { id: '/zeitstrahl/events/new' },
		params: {}
	} as never;
}

describe('zeitstrahl/events/new 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 an authenticated user without WRITE_ALL', async () => {
		await expect(load(loadEvent(['READ_ALL']))).rejects.toMatchObject({ status: 403 });
	});

	it('allows a curator with WRITE_ALL', async () => {
		vi.mocked(createApiClient).mockReturnValue({ GET: vi.fn() } as never);
		const result = await load(loadEvent(['WRITE_ALL']));
		expect(result.initialPersons).toEqual([]);
		expect(result.initialDocuments).toEqual([]);
	});
});

describe('zeitstrahl/events/new load — prefill (REQ-014)', () => {
	it('preselects a valid person and ignores an unknown document', async () => {
		const get = vi.fn((path: string) => {
			if (path === '/api/persons/{id}')
				return Promise.resolve({
					response: { ok: true },
					data: { id: 'p1', displayName: 'Anna' }
				});
			return Promise.resolve({ response: { ok: false }, data: null });
		});
		vi.mocked(createApiClient).mockReturnValue({ GET: get } as never);

		const result = await load(loadEvent(['WRITE_ALL'], '?personId=p1&documentId=missing'));
		expect(result.initialPersons).toHaveLength(1);
		expect(result.initialDocuments).toEqual([]);
		expect(result.originPersonId).toBe('p1');
	});
});

describe('zeitstrahl/events/new save action (REQ-004/009/010/015)', () => {
	const validUuid = '11111111-1111-1111-1111-111111111111';

	it('posts a TimelineEventRequest and redirects on success', async () => {
		const post = vi
			.fn()
			.mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'e-new' } });
		vi.mocked(createApiClient).mockReturnValue({ POST: post } as never);

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

		expect(post).toHaveBeenCalledTimes(1);
		expect(post.mock.calls[0][1].body).toMatchObject({
			title: 'Umzug',
			type: 'PERSONAL',
			eventDate: '1925-04-01'
		});
	});

	it('sends eventDateEnd: null when precision is not RANGE', async () => {
		const post = vi
			.fn()
			.mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'e-new' } });
		vi.mocked(createApiClient).mockReturnValue({ POST: post } as never);

		await expect(
			actions.save(
				saveEvent({
					title: 'Umzug',
					type: 'PERSONAL',
					eventDate: '1925-04-01',
					precision: 'YEAR',
					eventDateEnd: '1925-05-01'
				})
			)
		).rejects.toMatchObject({ status: 303 });
		expect(post.mock.calls[0][1].body.eventDateEnd).toBeNull();
	});

	it('falls back to DAY precision when an unknown precision token is submitted (REQ-009 hardening)', async () => {
		const post = vi
			.fn()
			.mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'e-new' } });
		vi.mocked(createApiClient).mockReturnValue({ POST: post } as never);

		await expect(
			actions.save(
				saveEvent({
					title: 'Umzug',
					type: 'PERSONAL',
					eventDate: '1925-04-01',
					precision: 'NOT_A_REAL_PRECISION'
				})
			)
		).rejects.toMatchObject({ status: 303 });
		expect(post.mock.calls[0][1].body.precision).toBe('DAY');
	});

	it('returns fail(400) with preserved + rehydrated pickers on blank title', async () => {
		const post = vi.fn();
		// On validation failure the action re-fetches the selected persons/documents
		// by id so the fail payload can rebuild full chips (Decision 6 / REQ-010).
		const get = vi.fn((path: string, opts: { params: { path: { id: string } } }) => {
			const id = opts.params.path.id;
			if (path === '/api/persons/{id}')
				return Promise.resolve({
					response: { ok: true },
					data: { id, displayName: `Person ${id}` }
				});
			return Promise.resolve({
				response: { ok: true },
				data: { id, title: `Doc ${id}`, documentDate: '1925-04-01' }
			});
		});
		vi.mocked(createApiClient).mockReturnValue({ POST: post, GET: get } as never);

		const result = await actions.save(
			saveEvent({
				title: '   ',
				type: 'PERSONAL',
				eventDate: '1925-04-01',
				personIds: ['p1', 'p2'],
				documentIds: ['d1']
			})
		);

		expect(post).not.toHaveBeenCalled();
		expect(result).toMatchObject({ status: 400 });
		expect(failData(result).personIds).toEqual(['p1', 'p2']);
		expect(failData(result).documentIds).toEqual(['d1']);
		expect(failData(result).titleError).toBeTruthy();
		// Rehydrated chips carry labels, not just ids.
		expect(failData(result).persons).toEqual([
			{ id: 'p1', displayName: 'Person p1' },
			{ id: 'p2', displayName: 'Person p2' }
		]);
		expect((failData(result).documents as { id: string }[])[0].id).toBe('d1');
	});

	it('preserves date, precision and end-date in the fail payload (review #2 — no-JS reload)', async () => {
		// A blank-title fail must echo back the entered When-section so a no-JS full
		// reload re-seeds it; otherwise the curator loses the date/precision/end-date.
		vi.mocked(createApiClient).mockReturnValue({ POST: vi.fn(), GET: vi.fn() } as never);

		const result = await actions.save(
			saveEvent({
				title: '',
				type: 'PERSONAL',
				eventDate: '1944-03-12',
				precision: 'RANGE',
				eventDateEnd: '1944-03-14'
			})
		);

		expect(result).toMatchObject({ status: 400 });
		expect(failData(result).eventDate).toBe('1944-03-12');
		expect(failData(result).precision).toBe('RANGE');
		expect(failData(result).eventDateEnd).toBe('1944-03-14');
	});

	it('rejects a RANGE event with no end date before calling the API (review #3)', async () => {
		// Without this guard the body reaches the backend, which 400s with a generic
		// INVALID_DATE_RANGE mapped to the wrong "end before start" message.
		const post = vi.fn();
		vi.mocked(createApiClient).mockReturnValue({ POST: post, GET: vi.fn() } as never);

		const result = await actions.save(
			saveEvent({
				title: 'Umzug',
				type: 'PERSONAL',
				eventDate: '1925-04-01',
				precision: 'RANGE'
				// eventDateEnd intentionally omitted
			})
		);

		expect(post).not.toHaveBeenCalled();
		expect(result).toMatchObject({ status: 400 });
		expect(failData(result).endDateError).toBeTruthy();
	});

	it('surfaces both title and date errors when both blank (REQ-011)', async () => {
		vi.mocked(createApiClient).mockReturnValue({ POST: vi.fn() } as never);
		const result = await actions.save(saveEvent({ title: '', type: 'PERSONAL', eventDate: '' }));
		expect(failData(result).titleError).toBeTruthy();
		expect(failData(result).dateError).toBeTruthy();
	});

	it('redirects to /persons/{id} when originPersonId is a valid UUID', async () => {
		const post = vi
			.fn()
			.mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'e-new' } });
		vi.mocked(createApiClient).mockReturnValue({ POST: post } as never);

		await expect(
			actions.save(
				saveEvent({
					title: 'Umzug',
					type: 'PERSONAL',
					eventDate: '1925-04-01',
					originPersonId: validUuid
				})
			)
		).rejects.toMatchObject({ status: 303, location: `/persons/${validUuid}` });
	});

	it('defaults to /zeitstrahl when originPersonId is not a valid UUID (REQ-015)', async () => {
		const post = vi
			.fn()
			.mockResolvedValue({ response: { ok: true, status: 200 }, data: { id: 'e-new' } });
		vi.mocked(createApiClient).mockReturnValue({ POST: post } as never);

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

	it('maps the API error and does not redirect on a non-ok save (incl. 409)', async () => {
		const post = vi.fn().mockResolvedValue({
			response: { ok: false, status: 409 },
			error: { code: 'CONFLICT' }
		});
		vi.mocked(createApiClient).mockReturnValue({ POST: post } as never);

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