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

const docSummary = (id: string, title: string) => ({
	id,
	title,
	datePrecision: 'DAY' as const,
	receiverCount: 0
});

const makeItem = (
	id: string,
	position: number,
	document?: ReturnType<typeof docSummary>,
	note?: string
) => ({ id, position, document, note });

/** DocumentListItem fixture as returned by the picker search endpoint. */
const makeSearchResultItem = (id: string, title: string) => ({
	id,
	title,
	documentDate: '1880-01-01',
	metaDatePrecision: 'DAY',
	originalFilename: 'brief.pdf',
	receivers: [],
	tags: [],
	completionPercentage: 0,
	contributors: [],
	matchData: {
		titleOffsets: [],
		senderMatched: false,
		matchedReceiverIds: [],
		matchedTagIds: [],
		snippetOffsets: [],
		summaryOffsets: []
	},
	status: 'UPLOADED',
	metadataComplete: false,
	scriptType: 'UNKNOWN',
	createdAt: '2024-01-01T00:00:00',
	updatedAt: '2024-01-01T00:00:00'
});

type MutationResponse = { ok: boolean; status?: number; body?: object };

/**
 * Routes the picker's GET search to `searchItems` and every mutation
 * (POST/DELETE) to `mutation` — the panel talks to both endpoints.
 */
function stubFetch(searchItems: object[], mutation: MutationResponse = { ok: true, body: {} }) {
	const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
		const method = (init?.method ?? 'GET').toUpperCase();
		if (method === 'GET') {
			return Promise.resolve({
				ok: true,
				json: () => Promise.resolve({ items: searchItems })
			});
		}
		return Promise.resolve({
			ok: mutation.ok,
			status: mutation.status ?? (mutation.ok ? 200 : 500),
			json: () => Promise.resolve(mutation.body ?? {})
		});
	});
	vi.stubGlobal('fetch', fetchMock);
	return fetchMock;
}

const defaultProps = (overrides: Record<string, unknown> = {}) => ({
	geschichteId: 'g1',
	items: [],
	...overrides
});

async function addViaPicker(title: RegExp) {
	await userEvent.fill(page.getByRole('combobox'), 'Brief');
	await expect.element(page.getByText(title)).toBeInTheDocument();
	await userEvent.click(page.getByText(title));
}

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

describe('StoryDocumentPanel — rendering', () => {
	it('renders linked documents sorted by position', async () => {
		render(
			StoryDocumentPanel,
			defaultProps({
				items: [
					makeItem('i3', 30, docSummary('d3', 'Dritter Brief')),
					makeItem('i1', 10, docSummary('d1', 'Erster Brief'))
				]
			})
		);

		const rows = Array.from(document.querySelectorAll('li')).map((li) => li.textContent ?? '');
		expect(rows[0]).toContain('Erster Brief');
		expect(rows[1]).toContain('Dritter Brief');
	});

	it('shows the empty state when no items are linked', async () => {
		render(StoryDocumentPanel, defaultProps());
		await expect.element(page.getByText(m.geschichte_documents_empty())).toBeInTheDocument();
	});

	it('renders a deleted-document item as placeholder row that is still removable', async () => {
		render(StoryDocumentPanel, defaultProps({ items: [makeItem('i1', 10, undefined)] }));

		await expect
			.element(page.getByText(m.geschichte_documents_deleted_placeholder()))
			.toBeInTheDocument();
		await expect
			.element(
				page.getByRole('button', {
					name: m.geschichte_documents_remove_label({
						title: m.geschichte_documents_deleted_placeholder()
					})
				})
			)
			.toBeInTheDocument();
	});

	it('wires a visible label to the picker input', async () => {
		render(StoryDocumentPanel, defaultProps());
		const input = page.getByRole('combobox').element() as HTMLInputElement;
		const label = document.querySelector(`label[for="${input.id}"]`);
		expect(label?.textContent).toContain(m.geschichte_documents_picker_label());
	});
});

describe('StoryDocumentPanel — add', () => {
	it('POSTs to the items endpoint and appends the created item', async () => {
		const fetchMock = stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')], {
			ok: true,
			body: makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))
		});
		render(StoryDocumentPanel, defaultProps());

		await addViaPicker(/Brief von Eugenie/i);

		const post = fetchMock.mock.calls.find(([, init]) => init?.method === 'POST');
		expect(post?.[0]).toBe('/api/geschichten/g1/items');
		expect(JSON.parse(String(post?.[1]?.body))).toEqual({ documentId: 'd1' });
		const rows = Array.from(document.querySelectorAll('li')).map((li) => li.textContent ?? '');
		expect(rows.some((r) => r.includes('Brief von Eugenie'))).toBe(true);
	});

	it('marks an already-linked document as not selectable in the dropdown', async () => {
		stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')]);
		render(
			StoryDocumentPanel,
			defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] })
		);

		await userEvent.fill(page.getByRole('combobox'), 'Brief');
		await expect.element(page.getByRole('option')).toBeInTheDocument();

		const option = document.querySelector('[role="listbox"] [role="option"]');
		expect(option?.getAttribute('aria-disabled')).toBe('true');
	});

	it('renders the story-worded duplicate error on a 409 JOURNEY_DOCUMENT_ALREADY_ADDED', async () => {
		stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')], {
			ok: false,
			status: 409,
			body: { code: 'JOURNEY_DOCUMENT_ALREADY_ADDED' }
		});
		render(StoryDocumentPanel, defaultProps());

		await addViaPicker(/Brief von Eugenie/i);

		await expect
			.element(page.getByRole('alert'))
			.toHaveTextContent(m.geschichte_documents_duplicate());
	});

	it('renders the story-worded capacity error on a 409 JOURNEY_AT_CAPACITY', async () => {
		stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')], {
			ok: false,
			status: 409,
			body: { code: 'JOURNEY_AT_CAPACITY' }
		});
		render(StoryDocumentPanel, defaultProps());

		await addViaPicker(/Brief von Eugenie/i);

		await expect
			.element(page.getByRole('alert'))
			.toHaveTextContent(m.geschichte_documents_capacity());
	});

	it('announces a successful add via the polite live region', async () => {
		stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')], {
			ok: true,
			body: makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))
		});
		render(StoryDocumentPanel, defaultProps());

		await addViaPicker(/Brief von Eugenie/i);

		const liveRegion = document.querySelector('[aria-live="polite"]');
		expect(liveRegion?.textContent).toBe(
			m.geschichte_documents_added_announce({ title: 'Brief von Eugenie' })
		);
	});

	it('routes a 403 response through getErrorMessage on POST', async () => {
		stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')], {
			ok: false,
			status: 403,
			body: { code: 'FORBIDDEN' }
		});
		render(StoryDocumentPanel, defaultProps());

		await addViaPicker(/Brief von Eugenie/i);

		await expect.element(page.getByRole('alert')).toBeInTheDocument();
		const alertText = page.getByRole('alert').element().textContent ?? '';
		expect(alertText).not.toBe('');
		expect(alertText).not.toContain('FORBIDDEN');
	});

	it('shows the generic reload message when POST throws a network error', async () => {
		stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')]);
		vi.stubGlobal(
			'fetch',
			vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
				if ((init?.method ?? 'GET').toUpperCase() === 'GET') {
					return Promise.resolve({
						ok: true,
						json: () =>
							Promise.resolve({ items: [makeSearchResultItem('d1', 'Brief von Eugenie')] })
					});
				}
				return Promise.reject(new Error('Network error'));
			})
		);
		render(StoryDocumentPanel, defaultProps());

		await addViaPicker(/Brief von Eugenie/i);

		await expect
			.element(page.getByRole('alert'))
			.toHaveTextContent(m.journey_mutation_error_reload());
	});

	it('attaches X-XSRF-TOKEN header from cookie on POST', async () => {
		document.cookie = 'XSRF-TOKEN=test-csrf-token';
		const fetchMock = stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')], {
			ok: true,
			body: makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))
		});
		render(StoryDocumentPanel, defaultProps());

		await addViaPicker(/Brief von Eugenie/i);

		const post = fetchMock.mock.calls.find(([, init]) => init?.method === 'POST');
		const headers = post?.[1]?.headers as Headers;
		expect(headers.get('X-XSRF-TOKEN')).toBe('test-csrf-token');
		document.cookie = 'XSRF-TOKEN=; Max-Age=0';
	});
});

describe('StoryDocumentPanel — remove', () => {
	it('DELETEs the item endpoint and removes the row', async () => {
		const fetchMock = stubFetch([], { ok: true, body: {} });
		render(
			StoryDocumentPanel,
			defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] })
		);

		await userEvent.click(
			page.getByRole('button', {
				name: m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' })
			})
		);

		const del = fetchMock.mock.calls.find(([, init]) => init?.method === 'DELETE');
		expect(del?.[0]).toBe('/api/geschichten/g1/items/i1');
		expect(document.querySelectorAll('li').length).toBe(0);
		await expect.element(page.getByText(m.geschichte_documents_empty())).toBeInTheDocument();
	});

	it('restores the row and shows an error when the DELETE fails', async () => {
		stubFetch([], { ok: false, status: 500, body: {} });
		render(
			StoryDocumentPanel,
			defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] })
		);

		await userEvent.click(
			page.getByRole('button', {
				name: m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' })
			})
		);

		await expect.element(page.getByRole('alert')).toBeInTheDocument();
		const rows = Array.from(document.querySelectorAll('li')).map((li) => li.textContent ?? '');
		expect(rows.some((r) => r.includes('Brief von Eugenie'))).toBe(true);
	});

	it('moves focus to the previous row remove button instead of dropping to body', async () => {
		stubFetch([], { ok: true, body: {} });
		render(
			StoryDocumentPanel,
			defaultProps({
				items: [
					makeItem('i1', 10, docSummary('d1', 'Erster Brief')),
					makeItem('i2', 20, docSummary('d2', 'Zweiter Brief'))
				]
			})
		);

		await userEvent.click(
			page.getByRole('button', {
				name: m.geschichte_documents_remove_label({ title: 'Zweiter Brief' })
			})
		);

		expect(document.activeElement).not.toBe(document.body);
		expect(document.activeElement?.getAttribute('aria-label')).toBe(
			m.geschichte_documents_remove_label({ title: 'Erster Brief' })
		);
	});

	it('moves focus to the picker input when the last item is removed', async () => {
		stubFetch([], { ok: true, body: {} });
		render(
			StoryDocumentPanel,
			defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] })
		);

		await userEvent.click(
			page.getByRole('button', {
				name: m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' })
			})
		);

		const input = page.getByRole('combobox').element();
		expect(document.activeElement).toBe(input);
	});

	it('announces a successful remove via the polite live region', async () => {
		stubFetch([], { ok: true, body: {} });
		render(
			StoryDocumentPanel,
			defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] })
		);

		await userEvent.click(
			page.getByRole('button', {
				name: m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' })
			})
		);

		const liveRegion = document.querySelector('[aria-live="polite"]');
		expect(liveRegion?.textContent).toBe(
			m.geschichte_documents_removed_announce({ title: 'Brief von Eugenie' })
		);
	});

	it('returns focus to the item remove button when DELETE fails with !res.ok', async () => {
		stubFetch([], { ok: false, status: 500, body: {} });
		render(
			StoryDocumentPanel,
			defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] })
		);

		await userEvent.click(
			page.getByRole('button', {
				name: m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' })
			})
		);

		expect(document.activeElement?.getAttribute('aria-label')).toBe(
			m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' })
		);
	});

	it('returns focus to the item remove button when DELETE throws a network error', async () => {
		vi.stubGlobal(
			'fetch',
			vi.fn(() => Promise.reject(new Error('Network error')))
		);
		render(
			StoryDocumentPanel,
			defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] })
		);

		await userEvent.click(
			page.getByRole('button', {
				name: m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' })
			})
		);

		expect(document.activeElement?.getAttribute('aria-label')).toBe(
			m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' })
		);
	});

	it('shows the generic reload message when DELETE throws a network error', async () => {
		vi.stubGlobal(
			'fetch',
			vi.fn(() => Promise.reject(new Error('Network error')))
		);
		render(
			StoryDocumentPanel,
			defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] })
		);

		await userEvent.click(
			page.getByRole('button', {
				name: m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' })
			})
		);

		await expect
			.element(page.getByRole('alert'))
			.toHaveTextContent(m.journey_mutation_error_reload());
	});

	it('attaches X-XSRF-TOKEN header from cookie on DELETE', async () => {
		document.cookie = 'XSRF-TOKEN=test-csrf-token';
		const fetchMock = stubFetch([], { ok: true, body: {} });
		render(
			StoryDocumentPanel,
			defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] })
		);

		await userEvent.click(
			page.getByRole('button', {
				name: m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' })
			})
		);

		const del = fetchMock.mock.calls.find(([, init]) => init?.method === 'DELETE');
		const headers = del?.[1]?.headers as Headers;
		expect(headers.get('X-XSRF-TOKEN')).toBe('test-csrf-token');
		document.cookie = 'XSRF-TOKEN=; Max-Age=0';
	});
});
