import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page, userEvent } from 'vitest/browser';
import DocumentMultiSelect from './DocumentMultiSelect.svelte';
import { m } from '$lib/paraglide/messages.js';

const waitForDebounce = () => new Promise((r) => setTimeout(r, 350));

const docFactory = (id: string, title: string, date = '1880-01-01') => ({
	id,
	title,
	documentDate: date,
	metaDatePrecision: 'DAY' as const,
	originalFilename: `${title}.pdf`,
	receivers: [],
	tags: [],
	completionPercentage: 0,
	contributors: [],
	matchData: {
		titleOffsets: [],
		senderMatched: false,
		matchedReceiverIds: [],
		matchedTagIds: [],
		snippetOffsets: [],
		summaryOffsets: []
	},
	status: 'UPLOADED' as const,
	metadataComplete: false,
	scriptType: 'UNKNOWN' as const,
	createdAt: '2024-01-01T00:00:00',
	updatedAt: '2024-01-01T00:00:00'
});

function mockSearchResponse(items: ReturnType<typeof docFactory>[]) {
	vi.stubGlobal(
		'fetch',
		vi.fn().mockResolvedValue({
			ok: true,
			json: vi.fn().mockResolvedValue({ items })
		})
	);
}

afterEach(() => {
	cleanup();
	vi.unstubAllGlobals();
});

describe('DocumentMultiSelect — rendering', () => {
	it('renders an empty chip-input by default', async () => {
		render(DocumentMultiSelect);
		await expect.element(page.getByPlaceholder('Dokument suchen…')).toBeInTheDocument();
	});

	it('renders pre-selected documents as chips with their date', async () => {
		render(DocumentMultiSelect, {
			selectedDocuments: [docFactory('d1', 'Brief vom 1. Mai', '1882-05-01')]
		});
		await expect.element(page.getByText(/Brief vom 1\. Mai/)).toBeInTheDocument();
		// DAY precision renders the honest long date (formatDocumentDate), not 01.05.1882.
		await expect.element(page.getByText(/1\. Mai 1882/)).toBeInTheDocument();
	});

	it('emits a hidden documentIds input for each pre-selected document', async () => {
		render(DocumentMultiSelect, {
			selectedDocuments: [docFactory('d1', 'A'), docFactory('d2', 'B')]
		});
		const inputs = document.querySelectorAll<HTMLInputElement>(
			'input[type="hidden"][name="documentIds"]'
		);
		expect(inputs).toHaveLength(2);
		expect([inputs[0].value, inputs[1].value].sort()).toEqual(['d1', 'd2']);
	});
});

describe('DocumentMultiSelect — search and select', () => {
	it('queries /api/documents/search after debounce and shows results', async () => {
		mockSearchResponse([docFactory('d1', 'Brief von Eugenie')]);
		render(DocumentMultiSelect);

		await userEvent.fill(page.getByPlaceholder('Dokument suchen…'), 'Eug');
		await waitForDebounce();

		expect(globalThis.fetch).toHaveBeenCalledWith(
			expect.stringMatching(/^\/api\/documents\/search\?q=Eug/)
		);
		await expect.element(page.getByText(/Brief von Eugenie/)).toBeInTheDocument();
	});

	it('adds a chip when a search result is clicked', async () => {
		mockSearchResponse([docFactory('d1', 'Brief von Eugenie')]);
		render(DocumentMultiSelect);

		await userEvent.fill(page.getByPlaceholder('Dokument suchen…'), 'Eug');
		await waitForDebounce();
		await userEvent.click(page.getByText(/Brief von Eugenie/));

		// After selection the search field clears and the chip is rendered
		const hidden = document.querySelector<HTMLInputElement>(
			'input[type="hidden"][name="documentIds"]'
		);
		expect(hidden?.value).toBe('d1');
	});

	it('hides already-selected documents from new search results', async () => {
		const fetchMock = vi.fn().mockResolvedValue({
			ok: true,
			json: vi.fn().mockResolvedValue({
				items: [docFactory('d1', 'Already attached'), docFactory('d2', 'Not attached')]
			})
		});
		vi.stubGlobal('fetch', fetchMock);

		render(DocumentMultiSelect, {
			selectedDocuments: [docFactory('d1', 'Already attached')]
		});

		await userEvent.fill(page.getByPlaceholder('Dokument suchen…'), 'attached');
		await waitForDebounce();

		// "Not attached" appears in the dropdown; "Already attached" only as the chip.
		const matches = await page.getByText(/Already attached/).all();
		expect(matches.length).toBe(1); // chip only, not in dropdown
		await expect.element(page.getByText(/Not attached/)).toBeInTheDocument();
	});
});

describe('DocumentMultiSelect — search failure', () => {
	it('shows an error row when the search request fails instead of looking like "no results"', async () => {
		vi.stubGlobal(
			'fetch',
			vi.fn().mockResolvedValue({
				ok: false,
				status: 500,
				json: vi.fn().mockResolvedValue({ code: 'INTERNAL_ERROR' })
			})
		);

		render(DocumentMultiSelect);

		await userEvent.fill(page.getByPlaceholder('Dokument suchen…'), 'Eug');
		await waitForDebounce();

		const alert = page.getByRole('alert');
		await expect.element(alert).toBeInTheDocument();
		await expect.element(alert).toHaveTextContent(m.comp_typeahead_error());
	});
});

describe('DocumentMultiSelect — remove', () => {
	it('removes a chip when its × button is clicked', async () => {
		render(DocumentMultiSelect, {
			selectedDocuments: [docFactory('d1', 'Brief A')]
		});
		await userEvent.click(page.getByLabelText('Entfernen'));
		expect(
			document.querySelector<HTMLInputElement>('input[type="hidden"][name="documentIds"]')
		).toBeNull();
	});

	// REQ-017 (#781): chip remove targets must be ≥44px for the 60+ audience.
	it('renders a ≥44px touch target on the chip remove button', async () => {
		render(DocumentMultiSelect, {
			selectedDocuments: [docFactory('d1', 'Brief A')]
		});
		const removeBtn = (await page.getByLabelText('Entfernen').element()) as HTMLElement;
		expect(removeBtn.className).toContain('min-h-[44px]');
		expect(removeBtn.className).toContain('min-w-[44px]');
	});
});
