/**
 * PersonMentionEditor — Tiptap-based component tests.
 *
 * All old tests used document.querySelector('textarea') which is dead after
 * the Tiptap migration. These tests drive the contenteditable via
 * userEvent.type() and inspect the serialized output from the test host.
 */
import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page, userEvent } from 'vitest/browser';
import PersonMentionEditorHost from './PersonMentionEditor.test-host.svelte';
import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js';

type Person = components['schemas']['Person'];
type PersonMention = components['schemas']['PersonMention'];

const AUGUSTE: Person = {
	id: 'p-aug',
	firstName: 'Auguste',
	lastName: 'Raddatz',
	displayName: 'Auguste Raddatz',
	birthYear: 1882,
	deathYear: 1944
} as unknown as Person;

const ANNA: Person = {
	id: 'p-anna',
	firstName: 'Anna',
	lastName: 'Schmidt',
	displayName: 'Anna Schmidt',
	birthYear: 1860
} as unknown as Person;

function mockFetchWithPersons(persons: Person[] = [AUGUSTE, ANNA]) {
	vi.stubGlobal(
		'fetch',
		vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue(persons) })
	);
}

function mockFetchEmpty() {
	vi.stubGlobal(
		'fetch',
		vi.fn().mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([]) })
	);
}

type Snapshot = { value: string; mentionedPersons: PersonMention[] };

function renderHost(
	initial: { value?: string; mentionedPersons?: PersonMention[]; disabled?: boolean } = {}
) {
	let snapshot: Snapshot = {
		value: initial.value ?? '',
		mentionedPersons: initial.mentionedPersons ?? []
	};
	render(PersonMentionEditorHost, {
		initialValue: initial.value ?? '',
		initialMentions: initial.mentionedPersons ?? [],
		disabled: initial.disabled ?? false,
		onChange: (snap: Snapshot) => {
			snapshot = snap;
		}
	});
	return {
		get snapshot() {
			return snapshot;
		}
	};
}

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

// ─── Rendering ────────────────────────────────────────────────────────────────

describe('PersonMentionEditor — rendering', () => {
	it('renders the editor as a textbox (ARIA role from editorProps)', async () => {
		render(PersonMentionEditorHost, {
			initialValue: '',
			initialMentions: [],
			onChange: () => {}
		});
		await expect.element(page.getByRole('textbox')).toBeInTheDocument();
	});

	it('reflects bound initial value as visible text', async () => {
		render(PersonMentionEditorHost, {
			initialValue: 'Hallo Welt',
			initialMentions: [],
			onChange: () => {}
		});
		await expect.element(page.getByText('Hallo Welt')).toBeInTheDocument();
	});
});

// ─── Typeahead opens on @ ─────────────────────────────────────────────────────

describe('PersonMentionEditor — typeahead', () => {
	it('opens the dropdown when typing @ + query and shows results', async () => {
		mockFetchWithPersons();
		renderHost();

		await userEvent.type(page.getByRole('textbox'), '@Aug');

		await vi.waitFor(async () => {
			await expect.element(page.getByText('Auguste Raddatz')).toBeInTheDocument();
		});
	});

	it('hits /api/persons?q= with the typed query', async () => {
		const fetchMock = vi
			.fn()
			.mockResolvedValue({ ok: true, json: vi.fn().mockResolvedValue([AUGUSTE]) });
		vi.stubGlobal('fetch', fetchMock);
		renderHost();

		await userEvent.type(page.getByRole('textbox'), '@Aug');

		await vi.waitFor(() => {
			expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/persons?q=Aug'));
		});
	});

	it('shows life dates next to the name in the dropdown', async () => {
		mockFetchWithPersons();
		renderHost();

		await userEvent.type(page.getByRole('textbox'), '@Aug');

		await vi.waitFor(async () => {
			await expect.element(page.getByText('* 1882 – † 1944')).toBeInTheDocument();
		});
	});

	it('shows empty state when no persons match', async () => {
		mockFetchEmpty();
		renderHost();

		await userEvent.type(page.getByRole('textbox'), '@xyz');

		await vi.waitFor(async () => {
			await expect.element(page.getByText('Keine Personen gefunden')).toBeInTheDocument();
		});
	});

	it('offers a "create new person" link in the empty state', async () => {
		mockFetchEmpty();
		renderHost();

		await userEvent.type(page.getByRole('textbox'), '@xyz');

		await vi.waitFor(async () => {
			const link = page.getByRole('link', { name: /Neue Person anlegen/ });
			await expect.element(link).toBeVisible();
			await expect.element(link).toHaveAttribute('href', '/persons/new');
		});
	});
});

// ─── AC-1: typed text becomes displayName, not DB name ───────────────────────

describe('PersonMentionEditor — AC-1: typed text as displayName', () => {
	it('stores the typed query as displayName, not the person DB name', async () => {
		mockFetchWithPersons();
		const host = renderHost();

		// User types "@Aug" (not the full "Auguste Raddatz") and selects Auguste Raddatz
		await userEvent.type(page.getByRole('textbox'), '@Aug');
		await vi.waitFor(async () => {
			await expect.element(page.getByRole('option', { name: /Auguste Raddatz/ })).toBeVisible();
		});

		await userEvent.click(page.getByRole('option', { name: /Auguste Raddatz/ }));

		await vi.waitFor(() => {
			expect(host.snapshot.mentionedPersons).toHaveLength(1);
			expect(host.snapshot.mentionedPersons[0]).toEqual({
				personId: 'p-aug',
				displayName: 'Aug' // typed text, not "Auguste Raddatz"
			});
		});
	});

	it('regression: text value contains the typed query, not the full DB name', async () => {
		mockFetchWithPersons();
		const host = renderHost();

		await userEvent.type(page.getByRole('textbox'), '@Aug');
		await vi.waitFor(async () => {
			await expect.element(page.getByRole('option', { name: /Auguste Raddatz/ })).toBeVisible();
		});

		await userEvent.click(page.getByRole('option', { name: /Auguste Raddatz/ }));

		await vi.waitFor(() => {
			// Text should contain "@Aug " (typed text + space), not "@Auguste Raddatz "
			expect(host.snapshot.value).toContain('@Aug');
			expect(host.snapshot.value).not.toContain('@Auguste Raddatz');
		});
	});

	it('pushes {personId, displayName} into mentionedPersons sidecar', async () => {
		mockFetchWithPersons();
		const host = renderHost();

		await userEvent.type(page.getByRole('textbox'), '@Aug');
		await vi.waitFor(async () => {
			await expect.element(page.getByRole('option', { name: /Auguste Raddatz/ })).toBeVisible();
		});

		await userEvent.click(page.getByRole('option', { name: /Auguste Raddatz/ }));

		await vi.waitFor(() => {
			expect(host.snapshot.mentionedPersons).toEqual([{ personId: 'p-aug', displayName: 'Aug' }]);
		});
	});

	it('does not duplicate the sidecar entry when the same person is selected twice', async () => {
		mockFetchWithPersons();
		const host = renderHost({
			value: '@Aug ',
			mentionedPersons: [{ personId: 'p-aug', displayName: 'Aug' }]
		});

		await userEvent.type(page.getByRole('textbox'), '@Aug');
		await vi.waitFor(async () => {
			await expect.element(page.getByRole('option', { name: /Auguste Raddatz/ })).toBeVisible();
		});

		await userEvent.click(page.getByRole('option', { name: /Auguste Raddatz/ }));

		await vi.waitFor(() => {
			expect(host.snapshot.mentionedPersons).toHaveLength(1);
		});
	});
});

// ─── Keyboard navigation ──────────────────────────────────────────────────────

describe('PersonMentionEditor — keyboard navigation', () => {
	it('Enter selects the highlighted result', async () => {
		mockFetchWithPersons();
		const host = renderHost();

		await userEvent.type(page.getByRole('textbox'), '@A');

		await vi.waitFor(async () => {
			await expect.element(page.getByRole('option', { name: /Auguste Raddatz/ })).toBeVisible();
		});

		await userEvent.keyboard('{Enter}');

		await vi.waitFor(() => {
			expect(host.snapshot.mentionedPersons).toHaveLength(1);
		});
	});

	it('ArrowDown moves the highlight to the next result', async () => {
		mockFetchWithPersons();
		renderHost();

		await userEvent.type(page.getByRole('textbox'), '@A');

		await vi.waitFor(async () => {
			await expect.element(page.getByRole('option', { name: /Auguste Raddatz/ })).toBeVisible();
		});

		await userEvent.keyboard('{ArrowDown}');

		await vi.waitFor(async () => {
			const annaOption = page.getByRole('option', { name: /Anna Schmidt/ });
			await expect.element(annaOption).toHaveAttribute('aria-selected', 'true');
		});
	});

	it('Escape closes the dropdown without inserting', async () => {
		mockFetchWithPersons();
		const host = renderHost();

		await userEvent.type(page.getByRole('textbox'), '@Aug');

		await vi.waitFor(async () => {
			await expect.element(page.getByText('Auguste Raddatz')).toBeInTheDocument();
		});

		await userEvent.keyboard('{Escape}');

		await vi.waitFor(async () => {
			await expect.element(page.getByRole('listbox')).not.toBeInTheDocument();
		});

		expect(host.snapshot.mentionedPersons).toEqual([]);
	});
});

// ─── Disabled state (WCAG 2.1.1 — keyboard users) ────────────────────────────

describe('PersonMentionEditor — disabled state', () => {
	it('sets contenteditable=false on the editor when disabled', async () => {
		renderHost({ value: 'Bestehender Text', disabled: true });

		await vi.waitFor(() => {
			const textbox = document.querySelector('[role="textbox"]') as HTMLElement | null;
			expect(textbox).not.toBeNull();
			expect(textbox!.getAttribute('contenteditable')).toBe('false');
		});
	});

	it('exposes aria-disabled=true on the editor wrapper when disabled', async () => {
		renderHost({ disabled: true });

		await vi.waitFor(() => {
			const wrapper = document.querySelector('[aria-disabled="true"]');
			expect(wrapper).not.toBeNull();
		});
	});

	it('keeps the editor editable (contenteditable=true) when not disabled', async () => {
		renderHost({ disabled: false });

		await vi.waitFor(() => {
			const textbox = document.querySelector('[role="textbox"]') as HTMLElement | null;
			expect(textbox).not.toBeNull();
			expect(textbox!.getAttribute('contenteditable')).toBe('true');
		});
	});
});

// ─── Security — XSS in displayName (CWE-79) ──────────────────────────────────

describe('PersonMentionEditor — XSS resistance', () => {
	it('renders a malicious displayName as text, not as HTML elements', async () => {
		// A historical sidecar entry whose displayName contains an HTML payload
		// that would execute if interpolated as raw HTML. Tiptap's renderHTML
		// returns the @-prefixed string as the third tuple entry, which
		// ProseMirror's DOMSerializer treats as a Text node — escaping it.
		const maliciousMention: PersonMention = {
			personId: '00000000-0000-0000-0000-000000000001',
			displayName: '<img src=x onerror=alert(1)>'
		};

		renderHost({
			value: '@<img src=x onerror=alert(1)>',
			mentionedPersons: [maliciousMention]
		});

		await vi.waitFor(() => {
			const textbox = document.querySelector('[role="textbox"]') as HTMLElement | null;
			expect(textbox).not.toBeNull();
			// No element from the malicious payload should have appeared as a real
			// DOM node. (Tiptap inserts its own ProseMirror-separator <img> in empty
			// paragraphs — that is internal markup and never carries user attrs;
			// guard against the injection by checking the user-controlled attrs.)
			expect(textbox!.querySelector('img[onerror]')).toBeNull();
			expect(textbox!.querySelector('img[src="x"]')).toBeNull();
			expect(textbox!.querySelector('script')).toBeNull();
			// The payload should appear as visible text content instead.
			expect(textbox!.textContent ?? '').toContain('<img src=x onerror=alert(1)>');
		});
	});
});

// ─── Placeholder behavior ─────────────────────────────────────────────────────

describe('PersonMentionEditor — placeholder behavior', () => {
	it('sets data-placeholder on the inner element when editor is empty', async () => {
		render(PersonMentionEditorHost, {
			initialValue: '',
			initialMentions: [],
			placeholder: 'Gib Text ein...',
			onChange: () => {}
		});
		await vi.waitFor(() => {
			const inner = document.querySelector('[data-editor-inner]') as HTMLElement | null;
			expect(inner).not.toBeNull();
			expect(inner!.getAttribute('data-placeholder')).toBe('Gib Text ein...');
		});
	});

	it('omits data-placeholder on the inner element when editor has content', async () => {
		render(PersonMentionEditorHost, {
			initialValue: 'Bestehender Text',
			initialMentions: [],
			placeholder: 'Gib Text ein...',
			onChange: () => {}
		});
		await vi.waitFor(() => {
			const inner = document.querySelector('[data-editor-inner]') as HTMLElement | null;
			expect(inner).not.toBeNull();
			expect(inner!.hasAttribute('data-placeholder')).toBe(false);
		});
	});
});

// ─── i18n message content ─────────────────────────────────────────────────────

describe('PersonMentionEditor — i18n message content', () => {
	it('transcription_block_placeholder contains @ mention trigger for discoverability', () => {
		expect(m.transcription_block_placeholder()).toContain('@');
	});
});

// ─── Touch target (WCAG 2.2 AA) ──────────────────────────────────────────────

describe('PersonMentionEditor — touch target', () => {
	it('each result row has min-h-[44px] (WCAG 2.2 AA)', async () => {
		mockFetchWithPersons();
		renderHost();

		await userEvent.type(page.getByRole('textbox'), '@Aug');

		await vi.waitFor(async () => {
			await expect.element(page.getByRole('option').first()).toBeVisible();
		});

		const option = document.querySelector('[role="option"]') as HTMLElement;
		expect(option).not.toBeNull();
		expect(option.className).toContain('min-h-[44px]');
	});
});
