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 { load } from './+page.server';
import { createApiClient } from '$lib/shared/api.server';

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

const VALID_UUID = '11111111-2222-3333-4444-555555555555';

function makeUrl(params: Record<string, string | string[]> = {}) {
	const url = new URL('http://localhost/geschichten');
	for (const [key, value] of Object.entries(params)) {
		if (Array.isArray(value)) {
			value.forEach((v) => url.searchParams.append(key, v));
		} else {
			url.searchParams.set(key, value);
		}
	}
	return url;
}

function callLoad(url: URL, parentData: { canBlogWrite?: boolean } = {}) {
	return load({
		url,
		request: new Request('http://localhost/geschichten'),
		fetch: vi.fn() as unknown as typeof fetch,
		parent: vi.fn().mockResolvedValue({
			canBlogWrite: false,
			canWrite: false,
			canAnnotate: false,
			user: null,
			...parentData
		})
	});
}

function mockApi(
	opts: {
		listData?: unknown[];
		docOk?: boolean;
		docData?: Record<string, unknown> | null;
	} = {}
) {
	const {
		listData = [],
		docOk = true,
		docData = { id: VALID_UUID, title: 'Brief an Oma', originalFilename: 'brief.jpg' }
	} = opts;

	const mockGet = vi.fn().mockImplementation((path: string) => {
		if (path === '/api/documents/{id}') {
			return Promise.resolve({
				response: { ok: docOk, status: docOk ? 200 : 404 },
				data: docOk ? docData : undefined
			});
		}
		return Promise.resolve({
			response: { ok: true, status: 200 },
			data: listData
		});
	});

	vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
		typeof createApiClient
	>);
	return mockGet;
}

describe('geschichten page load — documentFilter title resolution', () => {
	it('resolves document title when documentId is a valid UUID and document exists', async () => {
		mockApi({ docData: { id: VALID_UUID, title: 'Brief an Oma', originalFilename: 'brief.jpg' } });

		const result = await callLoad(makeUrl({ documentId: VALID_UUID }));

		expect(result.documentFilter).toEqual({ id: VALID_UUID, title: 'Brief an Oma' });
	});

	it('falls back to originalFilename when document title is null', async () => {
		mockApi({ docData: { id: VALID_UUID, title: null, originalFilename: 'scan_001.jpg' } });

		const result = await callLoad(makeUrl({ documentId: VALID_UUID }));

		expect(result.documentFilter).toEqual({ id: VALID_UUID, title: 'scan_001.jpg' });
	});

	it('preserves an empty-string title rather than falling back to filename', async () => {
		mockApi({ docData: { id: VALID_UUID, title: '', originalFilename: 'scan_001.jpg' } });

		const result = await callLoad(makeUrl({ documentId: VALID_UUID }));

		expect(result.documentFilter).toEqual({ id: VALID_UUID, title: '' });
	});

	it('degrades to {id, title: null} on 404 without throwing (resolves, never rejects)', async () => {
		// Explicit .resolves locks the no-throw guarantee — if error() were called, this would reject
		mockApi({ docOk: false });

		await expect(callLoad(makeUrl({ documentId: VALID_UUID }))).resolves.toMatchObject({
			documentFilter: { id: VALID_UUID, title: null }
		});
	});

	it('treats 403 identically to 404 — no oracle, loader still resolves', async () => {
		// Permanent regression test: loader must not call getErrorMessage/throw on a forbidden title fetch.
		// If it did, this assertion would fail with a rejection instead of a resolution.
		const mockGet = vi.fn().mockImplementation((path: string) => {
			if (path === '/api/documents/{id}') {
				return Promise.resolve({ response: { ok: false, status: 403 }, data: undefined });
			}
			return Promise.resolve({ response: { ok: true, status: 200 }, data: [] });
		});
		vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
			typeof createApiClient
		>);

		await expect(callLoad(makeUrl({ documentId: VALID_UUID }))).resolves.toMatchObject({
			documentFilter: { id: VALID_UUID, title: null }
		});
	});

	it('list still populates when title fetch returns 404 (independent results)', async () => {
		mockApi({
			listData: [{ id: 'g1', title: 'Some Story' }],
			docOk: false
		});

		const result = await callLoad(makeUrl({ documentId: VALID_UUID }));

		expect(result.geschichten).toHaveLength(1);
		expect(result.documentFilter).toEqual({ id: VALID_UUID, title: null });
	});

	it('returns null documentFilter when documentId is syntactically invalid', async () => {
		mockApi();

		const result = await callLoad(makeUrl({ documentId: 'not-a-uuid' }));

		expect(result.documentFilter).toBeNull();
	});

	it('does not fetch document title when documentId is invalid', async () => {
		const mockGet = mockApi();

		await callLoad(makeUrl({ documentId: 'not-a-uuid' }));

		expect(mockGet).not.toHaveBeenCalledWith('/api/documents/{id}', expect.anything());
	});

	it('returns null documentFilter when documentId is absent', async () => {
		mockApi();

		const result = await callLoad(makeUrl());

		expect(result.documentFilter).toBeNull();
	});

	it('passes valid documentId to the geschichten API', async () => {
		const mockGet = mockApi();

		await callLoad(makeUrl({ documentId: VALID_UUID }));

		expect(mockGet).toHaveBeenCalledWith(
			'/api/geschichten',
			expect.objectContaining({
				params: expect.objectContaining({
					query: expect.objectContaining({ documentId: VALID_UUID })
				})
			})
		);
	});

	it('omits documentId from the list API when the value is not a valid UUID', async () => {
		const mockGet = mockApi();

		await callLoad(makeUrl({ documentId: 'not-a-uuid' }));

		const listCall = mockGet.mock.calls.find((c) => c[0] === '/api/geschichten');
		expect(listCall?.[1]?.params?.query?.documentId).toBeUndefined();
	});

	it('keeps forwarding personId filters alongside documentId', async () => {
		const mockGet = mockApi();

		await callLoad(makeUrl({ documentId: VALID_UUID, personId: [VALID_UUID] }));

		expect(mockGet).toHaveBeenCalledWith(
			'/api/geschichten',
			expect.objectContaining({
				params: expect.objectContaining({
					query: expect.objectContaining({ documentId: VALID_UUID, personId: [VALID_UUID] })
				})
			})
		);
	});
});

// ─── draft fetch ──────────────────────────────────────────────────────────────

describe('geschichten page load — draft fetch', () => {
	it('makes a second API call for DRAFT stories when canBlogWrite is true', async () => {
		const mockGet = mockApi();

		await callLoad(makeUrl(), { canBlogWrite: true });

		const draftCall = mockGet.mock.calls.find(
			([, opts]: [string, { params?: { query?: { status?: string } } }]) =>
				opts?.params?.query?.status === 'DRAFT'
		);
		expect(draftCall).toBeDefined();
	});

	it('does NOT make a DRAFT API call when canBlogWrite is false', async () => {
		const mockGet = mockApi();

		await callLoad(makeUrl(), { canBlogWrite: false });

		const draftCall = mockGet.mock.calls.find(
			([, opts]: [string, { params?: { query?: { status?: string } } }]) =>
				opts?.params?.query?.status === 'DRAFT'
		);
		expect(draftCall).toBeUndefined();
	});

	it('returns empty drafts array when the DRAFT fetch rejects', async () => {
		const mockGet = vi
			.fn()
			.mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] })
			.mockRejectedValueOnce(new Error('network error'));
		vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
			typeof createApiClient
		>);

		const result = await callLoad(makeUrl(), { canBlogWrite: true });

		expect(result.drafts).toEqual([]);
	});

	it('returns drafts in page data when canBlogWrite is true', async () => {
		const draft = { id: 'draft-1', title: 'My Draft', status: 'DRAFT' };
		const mockGet = vi
			.fn()
			.mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] })
			.mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [draft] });
		vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
			typeof createApiClient
		>);

		const result = await callLoad(makeUrl(), { canBlogWrite: true });

		expect(result.drafts).toEqual([draft]);
	});
});
