import { describe, it, expect, afterEach, vi } from 'vitest';
import type { TranscribeShortcutOptions } from './transcribeShortcuts';

const { transcribeShortcuts } = await import('./transcribeShortcuts');

function makeOptions(
	overrides: Partial<TranscribeShortcutOptions> = {}
): TranscribeShortcutOptions {
	return {
		isPanelOpen: () => true,
		isCheatsheetOpen: () => false,
		panelMode: () => 'edit',
		goToNextRegion: vi.fn(),
		goToPrevRegion: vi.fn(),
		toggleMode: vi.fn(),
		closePanel: vi.fn(),
		startDrawMode: vi.fn(),
		toggleTrainingMark: vi.fn(),
		deleteCurrentRegion: vi.fn(),
		openCheatsheet: vi.fn(),
		...overrides
	};
}

describe('transcribeShortcuts action', () => {
	const nodes: HTMLElement[] = [];
	const teardowns: Array<() => void> = [];

	function makeNode(): HTMLElement {
		const node = document.createElement('div');
		document.body.appendChild(node);
		nodes.push(node);
		return node;
	}

	function attach(options: TranscribeShortcutOptions) {
		const node = makeNode();
		const action = transcribeShortcuts(node, options);
		teardowns.push(() => action.destroy());
		return action;
	}

	function press(
		key: string,
		opts: { target?: EventTarget; ctrlKey?: boolean; altKey?: boolean; metaKey?: boolean } = {}
	): KeyboardEvent {
		const event = new KeyboardEvent('keydown', {
			key,
			bubbles: true,
			cancelable: true,
			ctrlKey: opts.ctrlKey ?? false,
			altKey: opts.altKey ?? false,
			metaKey: opts.metaKey ?? false
		});
		(opts.target ?? document).dispatchEvent(event);
		return event;
	}

	function makeEditable(tag: 'input' | 'textarea' | 'div'): HTMLElement {
		const el = document.createElement(tag);
		if (tag === 'div') el.setAttribute('contenteditable', 'true');
		document.body.appendChild(el);
		nodes.push(el);
		return el;
	}

	afterEach(() => {
		teardowns.forEach((t) => t());
		teardowns.length = 0;
		nodes.forEach((n) => n.remove());
		nodes.length = 0;
	});

	describe('navigation and mode keys', () => {
		it('fires goToNextRegion on "j" and prevents default', () => {
			const options = makeOptions();
			attach(options);
			const event = press('j');
			expect(options.goToNextRegion).toHaveBeenCalledOnce();
			expect(event.defaultPrevented).toBe(true);
		});

		it('fires goToPrevRegion on "k"', () => {
			const options = makeOptions();
			attach(options);
			press('k');
			expect(options.goToPrevRegion).toHaveBeenCalledOnce();
		});

		it('fires toggleMode on "e"', () => {
			const options = makeOptions();
			attach(options);
			press('e');
			expect(options.toggleMode).toHaveBeenCalledOnce();
		});
	});

	describe('panel-open guard', () => {
		it('does not fire when the panel is closed', () => {
			const options = makeOptions({ isPanelOpen: () => false });
			attach(options);
			press('j');
			press('e');
			expect(options.goToNextRegion).not.toHaveBeenCalled();
			expect(options.toggleMode).not.toHaveBeenCalled();
		});
	});

	describe('focus guard', () => {
		it('does not fire "j" when focus is inside an <input>', () => {
			const options = makeOptions();
			attach(options);
			press('j', { target: makeEditable('input') });
			expect(options.goToNextRegion).not.toHaveBeenCalled();
		});

		it('does not fire "j"/"e"/"n"/"t" when focus is inside the TipTap contenteditable', () => {
			const options = makeOptions();
			attach(options);
			const editor = makeEditable('div');
			press('j', { target: editor });
			press('e', { target: editor });
			press('n', { target: editor });
			press('t', { target: editor });
			expect(options.goToNextRegion).not.toHaveBeenCalled();
			expect(options.toggleMode).not.toHaveBeenCalled();
			expect(options.startDrawMode).not.toHaveBeenCalled();
			expect(options.toggleTrainingMark).not.toHaveBeenCalled();
		});
	});

	describe('"?" cheatsheet (focus-independent)', () => {
		it('opens the cheatsheet on "?"', () => {
			const options = makeOptions();
			attach(options);
			const event = press('?');
			expect(options.openCheatsheet).toHaveBeenCalledOnce();
			expect(event.defaultPrevented).toBe(true);
		});

		it('opens the cheatsheet even when focus is inside the editor', () => {
			const options = makeOptions();
			attach(options);
			press('?', { target: makeEditable('div') });
			expect(options.openCheatsheet).toHaveBeenCalledOnce();
		});

		it('does nothing when a Ctrl/Alt/Meta modifier is held', () => {
			const options = makeOptions();
			attach(options);
			press('?', { ctrlKey: true });
			press('?', { altKey: true });
			press('?', { metaKey: true });
			expect(options.openCheatsheet).not.toHaveBeenCalled();
		});

		it('is a no-op when the cheatsheet is already open (open-only)', () => {
			const options = makeOptions({ isCheatsheetOpen: () => true });
			attach(options);
			press('?');
			expect(options.openCheatsheet).not.toHaveBeenCalled();
		});

		it('does not open the cheatsheet when the panel is closed', () => {
			const options = makeOptions({ isPanelOpen: () => false });
			attach(options);
			press('?');
			expect(options.openCheatsheet).not.toHaveBeenCalled();
		});
	});

	describe('"n" draw mode (edit only)', () => {
		it('is a no-op in read mode', () => {
			const options = makeOptions({ panelMode: () => 'read' });
			attach(options);
			press('n');
			expect(options.startDrawMode).not.toHaveBeenCalled();
		});

		it('fires startDrawMode in edit mode', () => {
			const options = makeOptions({ panelMode: () => 'edit' });
			attach(options);
			press('n');
			expect(options.startDrawMode).toHaveBeenCalledOnce();
		});
	});

	describe('"t" training mark', () => {
		it('fires toggleTrainingMark', () => {
			const options = makeOptions();
			attach(options);
			press('t');
			expect(options.toggleTrainingMark).toHaveBeenCalledOnce();
		});
	});

	describe('"Delete" current region — single owner', () => {
		it('fires deleteCurrentRegion exactly once when a focused annotation is the target', () => {
			const options = makeOptions();
			attach(options);
			const annotation = document.createElement('div');
			annotation.setAttribute('data-annotation', '');
			annotation.setAttribute('tabindex', '0');
			document.body.appendChild(annotation);
			nodes.push(annotation);
			press('Delete', { target: annotation });
			expect(options.deleteCurrentRegion).toHaveBeenCalledOnce();
		});
	});

	describe('Esc precedence ladder (decision B1)', () => {
		it('rung 1 — cheatsheet open: closePanel is NOT called (dialog handles Esc)', () => {
			const options = makeOptions({ isCheatsheetOpen: () => true });
			attach(options);
			press('Escape');
			expect(options.closePanel).not.toHaveBeenCalled();
		});

		it('rung 2 — focus inside editable: no callback fires', () => {
			const options = makeOptions();
			attach(options);
			press('Escape', { target: makeEditable('div') });
			expect(options.closePanel).not.toHaveBeenCalled();
		});

		it('rung 3 — otherwise: closePanel is called (pins panel-close)', () => {
			const options = makeOptions();
			attach(options);
			const event = press('Escape');
			expect(options.closePanel).toHaveBeenCalledOnce();
			expect(event.defaultPrevented).toBe(true);
		});
	});

	describe('lifecycle', () => {
		it('removes the listener on destroy (no leak)', () => {
			const options = makeOptions();
			const action = attach(options);
			action.destroy();
			press('j');
			expect(options.goToNextRegion).not.toHaveBeenCalled();
		});

		it('update() swaps callbacks so the listener never closes over stale state', () => {
			const first = makeOptions();
			const action = attach(first);
			const second = makeOptions();
			action.update(second);
			press('j');
			expect(first.goToNextRegion).not.toHaveBeenCalled();
			expect(second.goToNextRegion).toHaveBeenCalledOnce();
		});
	});
});
