import { describe, it, expect, vi, expectTypeOf } from 'vitest';
import { createBlockDragDrop } from './useBlockDragDrop.svelte';
import type { TranscriptionBlockData } from '$lib/shared/types';

// ---------------------------------------------------------------------------
// Type-regression guard: createBlockDragDrop must accept any T extends {id: string}
// so JourneyEditor can reuse it without importing TranscriptionBlockData.
// This test fails with "Expected 0 type arguments, but got 1" via tsc --noEmit
// until the function is made generic.
// ---------------------------------------------------------------------------
describe('createBlockDragDrop — generic type guard', () => {
	it('accepts items shaped as { id: string; position: number } — not only TranscriptionBlockData', () => {
		type SimpleItem = { id: string; position: number };
		const items: SimpleItem[] = [
			{ id: 'item-1', position: 0 },
			{ id: 'item-2', position: 1 }
		];
		const onReorder = vi.fn();
		const dd = createBlockDragDrop<SimpleItem>({ getSortedBlocks: () => items, onReorder });
		// Verify the hook is functional with the new type — state reads must work
		expect(dd.draggedBlockId).toBeNull();
		expect(dd.dragOffsetY).toBe(0);
	});

	it('TranscriptionBlockData caller still compiles — regression guard for existing transcription editor', () => {
		// If the generic constraint is wrong this line fails tsc --noEmit
		expectTypeOf(createBlockDragDrop<TranscriptionBlockData>).toBeFunction();
		// Runtime assertion so browser-mode doesn't report "no assertions"
		expect(typeof createBlockDragDrop).toBe('function');
	});
});

function makeBlock(id: string, sortOrder: number): TranscriptionBlockData {
	return {
		id,
		annotationId: `ann-${id}`,
		documentId: 'doc-1',
		text: '',
		label: null,
		sortOrder,
		version: 1,
		source: 'MANUAL',
		reviewed: false,
		mentionedPersons: []
	};
}

/**
 * Builds a DOM list, mocks getBoundingClientRect (60px per wrapper),
 * drags `dragId` and drops it so dropTargetIdx === targetIdx, then
 * triggers handlePointerUp. Returns the onReorder spy.
 */
function simulateDragDrop(
	dragId: string,
	targetIdx: number,
	blocks: TranscriptionBlockData[]
): ReturnType<typeof vi.fn> {
	const onReorder = vi.fn();
	const dd = createBlockDragDrop({ getSortedBlocks: () => blocks, onReorder });

	// Build DOM
	const listEl = document.createElement('div');
	const wrappers = blocks.map(() => {
		const grip = document.createElement('div');
		grip.setAttribute('data-drag-handle', '');
		const wrapper = document.createElement('div');
		wrapper.setAttribute('data-block-wrapper', '');
		wrapper.appendChild(grip);
		listEl.appendChild(wrapper);
		return { grip, wrapper };
	});
	document.body.appendChild(listEl);
	dd.setListElement(listEl);

	// Mock bounding rects: each wrapper is 60px tall starting at y=0
	wrappers.forEach(({ wrapper }, i) => {
		vi.spyOn(wrapper, 'getBoundingClientRect').mockReturnValue({
			top: i * 60,
			height: 60,
			bottom: (i + 1) * 60,
			left: 0,
			right: 100,
			width: 100,
			x: 0,
			y: i * 60,
			toJSON: () => ({})
		} as DOMRect);
	});

	const dragIdx = blocks.findIndex((b) => b.id === dragId);
	const { grip, wrapper: dragWrapper } = wrappers[dragIdx];
	dragWrapper.setPointerCapture = vi.fn();

	// Start drag
	const downEvent = new PointerEvent('pointerdown', { clientY: dragIdx * 60, cancelable: true });
	Object.defineProperty(downEvent, 'target', { value: grip });
	dd.handleGripDown(downEvent as PointerEvent, dragId);

	// Move pointer to achieve the desired targetIdx
	// midpoint of wrapper[i] = i*60 + 30
	// clientY just before midpoint[i] → target = i
	// clientY past last midpoint → target = wrappers.length
	let clientY: number;
	if (targetIdx <= 0) {
		clientY = 5; // before first midpoint (30)
	} else if (targetIdx >= wrappers.length) {
		clientY = wrappers.length * 60 + 10; // past all midpoints
	} else {
		clientY = targetIdx * 60 + 5; // just past top of wrapper[targetIdx], before its midpoint
	}

	const moveEvent = new PointerEvent('pointermove', { clientY });
	dd.handlePointerMove(moveEvent as PointerEvent);
	dd.handlePointerUp();

	document.body.removeChild(listEl);
	return onReorder;
}

describe('createBlockDragDrop', () => {
	it('initial state — no drag in progress', () => {
		const dd = createBlockDragDrop({ getSortedBlocks: () => [], onReorder: vi.fn() });
		expect(dd.draggedBlockId).toBeNull();
		expect(dd.dropTargetIdx).toBeNull();
		expect(dd.dragOffsetY).toBe(0);
	});

	it('handleGripDown sets draggedBlockId when grip is hit', () => {
		const dd = createBlockDragDrop({ getSortedBlocks: () => [], onReorder: vi.fn() });
		const grip = document.createElement('div');
		grip.setAttribute('data-drag-handle', '');
		const wrapper = document.createElement('div');
		wrapper.setAttribute('data-block-wrapper', '');
		wrapper.appendChild(grip);
		document.body.appendChild(wrapper);

		const e = new PointerEvent('pointerdown', { clientY: 100, cancelable: true, bubbles: true });
		Object.defineProperty(e, 'target', { value: grip });
		wrapper.setPointerCapture = vi.fn();

		dd.handleGripDown(e as PointerEvent, 'block-1');
		expect(dd.draggedBlockId).toBe('block-1');

		document.body.removeChild(wrapper);
	});

	it('handlePointerUp without active drag is a no-op', () => {
		const onReorder = vi.fn();
		const dd = createBlockDragDrop({ getSortedBlocks: () => [], onReorder });
		dd.handlePointerUp();
		expect(onReorder).not.toHaveBeenCalled();
	});

	it('handlePointerUp with null dropTargetIdx does not call onReorder', () => {
		const onReorder = vi.fn();
		const blocks = [makeBlock('b1', 0), makeBlock('b2', 1)];
		const dd = createBlockDragDrop({ getSortedBlocks: () => blocks, onReorder });

		const grip = document.createElement('div');
		grip.setAttribute('data-drag-handle', '');
		const wrapper = document.createElement('div');
		wrapper.setAttribute('data-block-wrapper', '');
		wrapper.appendChild(grip);
		document.body.appendChild(wrapper);
		wrapper.setPointerCapture = vi.fn();

		const downEvent = new PointerEvent('pointerdown', { clientY: 50, cancelable: true });
		Object.defineProperty(downEvent, 'target', { value: grip });
		dd.handleGripDown(downEvent as PointerEvent, 'b1');

		// dropTargetIdx is still null (no pointer move happened)
		dd.handlePointerUp();
		expect(onReorder).not.toHaveBeenCalled();
		expect(dd.draggedBlockId).toBeNull();

		document.body.removeChild(wrapper);
	});

	it('reorder: moves block from index 0 to end', () => {
		const blocks = [makeBlock('b1', 0), makeBlock('b2', 1), makeBlock('b3', 2)];
		const onReorder = simulateDragDrop('b1', 3, blocks);
		expect(onReorder).toHaveBeenCalledWith(['b2', 'b3', 'b1']);
	});

	it('reorder: moves block from end to index 0', () => {
		const blocks = [makeBlock('b1', 0), makeBlock('b2', 1), makeBlock('b3', 2)];
		const onReorder = simulateDragDrop('b3', 0, blocks);
		expect(onReorder).toHaveBeenCalledWith(['b3', 'b1', 'b2']);
	});

	it('reorder: moves block down by one position (tests insertAt = dropTargetIdx - 1)', () => {
		const blocks = [makeBlock('b1', 0), makeBlock('b2', 1), makeBlock('b3', 2)];
		// dragId=b1 (idx=0), targetIdx=2 → insertAt = 2-1 = 1 → [b2, b1, b3]
		const onReorder = simulateDragDrop('b1', 2, blocks);
		expect(onReorder).toHaveBeenCalledWith(['b2', 'b1', 'b3']);
	});
});
