import { describe, it, expect } from 'vitest';
import {
	matchSpecificity,
	resolveGesture,
	resolveGestureIndexed,
	buildRuleIndex,
	formatRuleLabel,
	mergeRules,
	DEFAULT_GESTURE_RULES,
	type GestureDescriptor,
	type GesturePattern,
	type GestureRule,
} from '../core/gesture-rules';

// =============================================================================
// matchSpecificity
// =============================================================================

describe('matchSpecificity', () => {
	it('wildcard pattern matches anything with score 0', () => {
		const pattern: GesturePattern = {};
		const desc: GestureDescriptor = { gesture: 'tap', target: 'node', source: 'mouse' };
		expect(matchSpecificity(pattern, desc)).toBe(0);
	});

	it('gesture match adds 32', () => {
		expect(matchSpecificity({ gesture: 'tap' }, { gesture: 'tap', target: 'node', source: 'mouse' })).toBe(32);
	});

	it('target match adds 16', () => {
		expect(matchSpecificity({ target: 'node' }, { gesture: 'tap', target: 'node', source: 'mouse' })).toBe(16);
	});

	it('source match adds 4', () => {
		expect(matchSpecificity({ source: 'mouse' }, { gesture: 'tap', target: 'node', source: 'mouse' })).toBe(4);
	});

	it('button match adds 2', () => {
		expect(matchSpecificity({ button: 2 }, { gesture: 'tap', target: 'node', source: 'mouse', button: 2 })).toBe(2);
	});

	it('button 0 matches descriptor without button (default)', () => {
		expect(matchSpecificity({ button: 0 }, { gesture: 'tap', target: 'node', source: 'mouse' })).toBe(2);
	});

	it('modifier match adds 8 per flag', () => {
		const pattern: GesturePattern = { modifiers: { shift: true, ctrl: true } };
		const desc: GestureDescriptor = { gesture: 'drag', target: 'background', source: 'mouse', modifiers: { shift: true, ctrl: true } };
		expect(matchSpecificity(pattern, desc)).toBe(16); // 2 modifier flags x 8
	});

	it('modifier rules beat source-specific rules', () => {
		const modPattern: GesturePattern = { gesture: 'drag', target: 'background', modifiers: { shift: true } };
		const srcPattern: GesturePattern = { gesture: 'drag', target: 'background', source: 'mouse' };
		const desc: GestureDescriptor = { gesture: 'drag', target: 'background', source: 'mouse', modifiers: { shift: true } };
		expect(matchSpecificity(modPattern, desc)).toBeGreaterThan(matchSpecificity(srcPattern, desc));
	});

	it('full match scores high', () => {
		const pattern: GesturePattern = { gesture: 'drag', target: 'node', source: 'mouse', button: 0, modifiers: { shift: true } };
		const desc: GestureDescriptor = { gesture: 'drag', target: 'node', source: 'mouse', button: 0, modifiers: { shift: true } };
		expect(matchSpecificity(pattern, desc)).toBe(32 + 16 + 4 + 2 + 8);
	});

	it('returns -1 on gesture mismatch', () => {
		expect(matchSpecificity({ gesture: 'tap' }, { gesture: 'drag', target: 'node', source: 'mouse' })).toBe(-1);
	});

	it('returns -1 on target mismatch', () => {
		expect(matchSpecificity({ target: 'edge' }, { gesture: 'tap', target: 'node', source: 'mouse' })).toBe(-1);
	});

	it('returns -1 on source mismatch', () => {
		expect(matchSpecificity({ source: 'pencil' }, { gesture: 'tap', target: 'node', source: 'mouse' })).toBe(-1);
	});

	it('returns -1 on button mismatch', () => {
		expect(matchSpecificity({ button: 2 }, { gesture: 'tap', target: 'node', source: 'mouse', button: 0 })).toBe(-1);
	});

	it('returns -1 on modifier mismatch', () => {
		const pattern: GesturePattern = { modifiers: { shift: true } };
		const desc: GestureDescriptor = { gesture: 'drag', target: 'background', source: 'mouse', modifiers: { shift: false } };
		expect(matchSpecificity(pattern, desc)).toBe(-1);
	});

	it('returns -1 when pattern requires modifier absent from descriptor', () => {
		const pattern: GesturePattern = { modifiers: { shift: true } };
		const desc: GestureDescriptor = { gesture: 'drag', target: 'background', source: 'mouse' };
		expect(matchSpecificity(pattern, desc)).toBe(-1);
	});
});

// =============================================================================
// resolveGesture
// =============================================================================

describe('resolveGesture', () => {
	const rules: GestureRule[] = [
		{ id: 'drag-node', pattern: { gesture: 'drag', target: 'node' }, actionId: 'move-node' },
		{ id: 'drag-node-right', pattern: { gesture: 'drag', target: 'node', button: 2 }, actionId: 'open-radial' },
		{ id: 'drag-node-mouse-shift', pattern: { gesture: 'drag', target: 'node', source: 'mouse', modifiers: { shift: true } }, actionId: 'duplicate-node' },
		{ id: 'tap-node', pattern: { gesture: 'tap', target: 'node' }, actionId: 'select-node' },
		{ id: 'drag-bg', pattern: { gesture: 'drag', target: 'background' }, actionId: 'pan' },
	];

	it('resolves most specific match', () => {
		const result = resolveGesture(
			{ gesture: 'drag', target: 'node', source: 'mouse', button: 2 },
			rules,
		);
		expect(result?.actionId).toBe('open-radial');
	});

	it('falls back to less specific rule', () => {
		const result = resolveGesture(
			{ gesture: 'drag', target: 'node', source: 'finger' },
			rules,
		);
		expect(result?.actionId).toBe('move-node');
	});

	it('resolves modifier-qualified rule', () => {
		const result = resolveGesture(
			{ gesture: 'drag', target: 'node', source: 'mouse', modifiers: { shift: true } },
			rules,
		);
		expect(result?.actionId).toBe('duplicate-node');
	});

	it('returns null for no match', () => {
		const result = resolveGesture(
			{ gesture: 'pinch', target: 'port', source: 'mouse' },
			rules,
		);
		expect(result).toBeNull();
	});

	it('uses priority as tiebreaker', () => {
		const tiedRules: GestureRule[] = [
			{ id: 'a', pattern: { gesture: 'tap', target: 'node' }, actionId: 'action-a', priority: 1 },
			{ id: 'b', pattern: { gesture: 'tap', target: 'node' }, actionId: 'action-b', priority: 10 },
		];
		const result = resolveGesture({ gesture: 'tap', target: 'node', source: 'mouse' }, tiedRules);
		expect(result?.actionId).toBe('action-b');
	});

	describe('palm rejection', () => {
		it('rejects finger taps when stylus active', () => {
			const result = resolveGesture(
				{ gesture: 'tap', target: 'node', source: 'finger', isStylusActive: true },
				rules,
			);
			expect(result?.actionId).toBe('none');
		});

		it('rejects finger long-press when stylus active', () => {
			const longPressRules: GestureRule[] = [
				{ id: 'lp', pattern: { gesture: 'long-press', target: 'node' }, actionId: 'context-menu' },
			];
			const result = resolveGesture(
				{ gesture: 'long-press', target: 'node', source: 'finger', isStylusActive: true },
				longPressRules,
			);
			expect(result?.actionId).toBe('none');
		});

		it('rejects finger triple-tap when stylus active', () => {
			const tripleTapRules: GestureRule[] = [
				{ id: 'ttap', pattern: { gesture: 'triple-tap', target: 'node' }, actionId: 'toggle-lock' },
			];
			const result = resolveGesture(
				{ gesture: 'triple-tap', target: 'node', source: 'finger', isStylusActive: true },
				tripleTapRules,
			);
			expect(result?.actionId).toBe('none');
		});

		it('remaps finger drag on node to background pan when stylus active', () => {
			const result = resolveGesture(
				{ gesture: 'drag', target: 'node', source: 'finger', isStylusActive: true },
				rules,
			);
			expect(result?.actionId).toBe('pan');
		});

		it('allows finger background drag even when stylus active', () => {
			const result = resolveGesture(
				{ gesture: 'drag', target: 'background', source: 'finger', isStylusActive: true },
				rules,
			);
			expect(result?.actionId).toBe('pan');
		});

		it('does not reject pencil gestures when stylus active', () => {
			const result = resolveGesture(
				{ gesture: 'tap', target: 'node', source: 'pencil', isStylusActive: true },
				rules,
			);
			expect(result?.actionId).toBe('select-node');
		});

		it('can be disabled', () => {
			const result = resolveGesture(
				{ gesture: 'tap', target: 'node', source: 'finger', isStylusActive: true },
				rules,
				{ palmRejection: false },
			);
			expect(result?.actionId).toBe('select-node');
		});
	});
});

// =============================================================================
// resolveGestureIndexed
// =============================================================================

describe('resolveGestureIndexed', () => {
	const index = buildRuleIndex(DEFAULT_GESTURE_RULES);

	it('resolves tap on node to select-node', () => {
		const result = resolveGestureIndexed(
			{ gesture: 'tap', target: 'node', source: 'mouse' },
			index,
		);
		expect(result?.actionId).toBe('select-node');
	});

	it('resolves drag on background with mouse to pan', () => {
		const result = resolveGestureIndexed(
			{ gesture: 'drag', target: 'background', source: 'mouse' },
			index,
		);
		expect(result?.actionId).toBe('pan');
	});

	it('resolves drag on background with pencil to lasso-select', () => {
		const result = resolveGestureIndexed(
			{ gesture: 'drag', target: 'background', source: 'pencil' },
			index,
		);
		expect(result?.actionId).toBe('lasso-select');
	});

	it('resolves right-button tap on node to context menu', () => {
		const result = resolveGestureIndexed(
			{ gesture: 'tap', target: 'node', source: 'mouse', button: 2 },
			index,
		);
		expect(result?.actionId).toBe('open-context-menu');
	});

	it('resolves right-button drag on node to none (default)', () => {
		const result = resolveGestureIndexed(
			{ gesture: 'drag', target: 'node', source: 'mouse', button: 2 },
			index,
		);
		expect(result?.actionId).toBe('none');
	});

	it('resolves shift+drag on background to rect-select', () => {
		const result = resolveGestureIndexed(
			{ gesture: 'drag', target: 'background', source: 'mouse', modifiers: { shift: true } },
			index,
		);
		expect(result?.actionId).toBe('rect-select');
	});

	it('resolves double-tap on node to fit-to-view (not toggle-lock)', () => {
		const result = resolveGestureIndexed(
			{ gesture: 'double-tap', target: 'node', source: 'mouse' },
			index,
		);
		expect(result?.actionId).toBe('fit-to-view');
	});

	it('resolves triple-tap on node to toggle-lock', () => {
		const result = resolveGestureIndexed(
			{ gesture: 'triple-tap', target: 'node', source: 'mouse' },
			index,
		);
		expect(result?.actionId).toBe('toggle-lock');
	});
});

// =============================================================================
// formatRuleLabel
// =============================================================================

describe('formatRuleLabel', () => {
	it('formats gesture + target', () => {
		expect(formatRuleLabel({ gesture: 'drag', target: 'node' })).toBe('Drag on node');
	});

	it('formats with source', () => {
		expect(formatRuleLabel({ gesture: 'drag', target: 'background', source: 'pencil' })).toBe('Pencil Drag on background');
	});

	it('formats with button', () => {
		expect(formatRuleLabel({ gesture: 'tap', target: 'node', button: 2 })).toBe('Right Tap on node');
	});

	it('formats with modifiers', () => {
		expect(formatRuleLabel({ gesture: 'drag', target: 'node', modifiers: { shift: true } })).toBe('Shift + drag on node');
	});

	it('formats wildcard as "Any gesture"', () => {
		expect(formatRuleLabel({})).toBe('Any gesture');
	});
});

// =============================================================================
// mergeRules
// =============================================================================

describe('mergeRules', () => {
	const defaults: GestureRule[] = [
		{ id: 'a', pattern: { gesture: 'tap', target: 'node' }, actionId: 'select' },
		{ id: 'b', pattern: { gesture: 'drag', target: 'node' }, actionId: 'move' },
	];

	it('overrides rules with matching IDs', () => {
		const overrides: GestureRule[] = [
			{ id: 'a', pattern: { gesture: 'tap', target: 'node' }, actionId: 'custom-select' },
		];
		const result = mergeRules(defaults, overrides);
		expect(result.find((r) => r.id === 'a')?.actionId).toBe('custom-select');
		expect(result).toHaveLength(2);
	});

	it('appends new rules', () => {
		const overrides: GestureRule[] = [
			{ id: 'c', pattern: { gesture: 'tap', target: 'edge' }, actionId: 'select-edge' },
		];
		const result = mergeRules(defaults, overrides);
		expect(result).toHaveLength(3);
		expect(result[2].id).toBe('c');
	});

	it('preserves order of defaults', () => {
		const result = mergeRules(defaults, []);
		expect(result.map((r) => r.id)).toEqual(['a', 'b']);
	});
});

// =============================================================================
// DEFAULT_GESTURE_RULES consistency
// =============================================================================

describe('DEFAULT_GESTURE_RULES', () => {
	it('has unique IDs', () => {
		const ids = DEFAULT_GESTURE_RULES.map((r) => r.id);
		expect(new Set(ids).size).toBe(ids.length);
	});

	it('replicates existing behavior: mouse drag on node = move-node', () => {
		const result = resolveGesture(
			{ gesture: 'drag', target: 'node', source: 'mouse' },
			DEFAULT_GESTURE_RULES,
		);
		expect(result?.actionId).toBe('move-node');
	});

	it('replicates existing behavior: pencil drag on bg = lasso-select', () => {
		const result = resolveGesture(
			{ gesture: 'drag', target: 'background', source: 'pencil' },
			DEFAULT_GESTURE_RULES,
		);
		expect(result?.actionId).toBe('lasso-select');
	});

	it('replicates existing behavior: finger drag on bg = pan', () => {
		const result = resolveGesture(
			{ gesture: 'drag', target: 'background', source: 'finger' },
			DEFAULT_GESTURE_RULES,
		);
		expect(result?.actionId).toBe('pan');
	});
});
