import { describe, it, expect } from 'vitest';
import { buildMappingIndex, resolve } from '../mapper';
import type { GestureEvent, GuardContext, KeyInputEvent, MappingContext } from '../types';
import { NO_HELD_KEYS, NO_MODIFIERS } from '../types';

function makeEvent(overrides: Partial<GestureEvent> = {}): GestureEvent {
	return {
		type: 'tap',
		phase: 'instant',
		subject: { kind: 'node', nodeId: 'n1' },
		source: 'mouse',
		button: 0,
		modifiers: NO_MODIFIERS,
		heldKeys: NO_HELD_KEYS,
		screenPosition: { x: 0, y: 0 },
		worldPosition: { x: 0, y: 0 },
		...overrides,
	};
}

function makeKeyEvent(overrides: Partial<KeyInputEvent> = {}): KeyInputEvent {
	return {
		kind: 'key',
		phase: 'down',
		key: 'a',
		code: 'KeyA',
		repeat: false,
		modifiers: NO_MODIFIERS,
		heldKeys: NO_HELD_KEYS,
		subject: { kind: 'background' },
		...overrides,
	};
}

const defaultGuard: GuardContext = {
	isStylusActive: false,
	fingerCount: 0,
	isDragging: false,
	isResizing: false,
	isSplitting: false,
	inputMode: { type: 'normal' },
	keyboardInteractionMode: 'navigate',
	selectedNodeIds: new Set(),
	focusedNodeId: null,
	isSearchActive: false,
	commandLineVisible: false,
	heldKeys: NO_HELD_KEYS,
	custom: {},
};

describe('mapper', () => {
	it('resolves a simple binding', () => {
		const ctx: MappingContext = {
			id: 'test',
			priority: 100,
			enabled: true,
			bindings: [
				{ id: 'b1', pattern: { type: 'tap', subjectKind: 'node' }, actionId: 'select-node' },
			],
		};
		const index = buildMappingIndex([ctx]);
		const result = resolve(makeEvent(), index, defaultGuard);
		expect(result).not.toBeNull();
		expect(result!.actionId).toBe('select-node');
		expect(result!.contextId).toBe('test');
	});

	it('returns null when no binding matches', () => {
		const ctx: MappingContext = {
			id: 'test',
			priority: 100,
			enabled: true,
			bindings: [
				{ id: 'b1', pattern: { type: 'drag' }, actionId: 'move' },
			],
		};
		const index = buildMappingIndex([ctx]);
		const result = resolve(makeEvent(), index, defaultGuard);
		expect(result).toBeNull();
	});

	it('higher specificity wins within same context', () => {
		const ctx: MappingContext = {
			id: 'test',
			priority: 100,
			enabled: true,
			bindings: [
				{ id: 'generic', pattern: { type: 'tap' }, actionId: 'generic-tap' },
				{ id: 'specific', pattern: { type: 'tap', subjectKind: 'node' }, actionId: 'node-tap' },
			],
		};
		const index = buildMappingIndex([ctx]);
		const result = resolve(makeEvent(), index, defaultGuard);
		expect(result!.actionId).toBe('node-tap');
	});

	it('higher-priority context wins over lower-priority', () => {
		const high: MappingContext = {
			id: 'high',
			priority: 10,
			enabled: true,
			bindings: [
				{ id: 'h1', pattern: { type: 'tap', subjectKind: 'node' }, actionId: 'high-action' },
			],
		};
		const low: MappingContext = {
			id: 'low',
			priority: 100,
			enabled: true,
			bindings: [
				{ id: 'l1', pattern: { type: 'tap', subjectKind: 'node' }, actionId: 'low-action' },
			],
		};
		const index = buildMappingIndex([low, high]); // order shouldn't matter
		const result = resolve(makeEvent(), index, defaultGuard);
		expect(result!.actionId).toBe('high-action');
	});

	it('consumeInput blocks lower-priority contexts', () => {
		const high: MappingContext = {
			id: 'consumer',
			priority: 10,
			enabled: true,
			bindings: [
				{ id: 'c1', pattern: { type: 'tap' }, actionId: 'consumed', consumeInput: true },
			],
		};
		const low: MappingContext = {
			id: 'default',
			priority: 100,
			enabled: true,
			bindings: [
				{ id: 'd1', pattern: { type: 'tap', subjectKind: 'node' }, actionId: 'unreachable' },
			],
		};
		const index = buildMappingIndex([high, low]);
		const result = resolve(makeEvent(), index, defaultGuard);
		expect(result!.actionId).toBe('consumed');
		expect(result!.consumed).toBe(true);
	});

	it('disabled context is skipped', () => {
		const disabled: MappingContext = {
			id: 'disabled',
			priority: 10,
			enabled: false,
			bindings: [
				{ id: 'd1', pattern: { type: 'tap' }, actionId: 'should-skip' },
			],
		};
		const fallback: MappingContext = {
			id: 'fallback',
			priority: 100,
			enabled: true,
			bindings: [
				{ id: 'f1', pattern: { type: 'tap' }, actionId: 'fallback' },
			],
		};
		const index = buildMappingIndex([disabled, fallback]);
		const result = resolve(makeEvent(), index, defaultGuard);
		expect(result!.actionId).toBe('fallback');
	});

	it('when guard can reject a binding', () => {
		const ctx: MappingContext = {
			id: 'guarded',
			priority: 100,
			enabled: true,
			bindings: [
				{
					id: 'g1',
					pattern: { type: 'tap' },
					actionId: 'guarded-action',
					when: (g) => g.isStylusActive,
				},
				{ id: 'g2', pattern: { type: 'tap' }, actionId: 'fallback-action' },
			],
		};
		const index = buildMappingIndex([ctx]);
		// Guard fails — stylus not active
		const result = resolve(makeEvent(), index, defaultGuard);
		expect(result!.actionId).toBe('fallback-action');

		// Guard passes
		const result2 = resolve(makeEvent(), index, { ...defaultGuard, isStylusActive: true });
		expect(result2!.actionId).toBe('guarded-action');
	});

	it('wildcard binding matches any gesture type', () => {
		const ctx: MappingContext = {
			id: 'catch-all',
			priority: 100,
			enabled: true,
			bindings: [
				{ id: 'w1', pattern: { subjectKind: 'node' }, actionId: 'catch-all' },
			],
		};
		const index = buildMappingIndex([ctx]);

		const tapResult = resolve(makeEvent({ type: 'tap' }), index, defaultGuard);
		expect(tapResult!.actionId).toBe('catch-all');

		const dragResult = resolve(makeEvent({ type: 'drag' }), index, defaultGuard);
		expect(dragResult!.actionId).toBe('catch-all');
	});

	it('matches keyboard bindings by key', () => {
		const ctx: MappingContext = {
			id: 'keyboard',
			priority: 100,
			enabled: true,
			bindings: [
				{ id: 'k1', pattern: { kind: 'key', phase: 'down', key: 'a' }, actionId: 'alpha' },
			],
		};
		const index = buildMappingIndex([ctx]);
		const result = resolve(makeKeyEvent(), index, defaultGuard);
		expect(result?.actionId).toBe('alpha');
	});

	it('matches keyboard bindings by code', () => {
		const ctx: MappingContext = {
			id: 'keyboard',
			priority: 100,
			enabled: true,
			bindings: [
				{ id: 'k1', pattern: { kind: 'key', phase: 'down', code: 'KeyA' }, actionId: 'alpha' },
			],
		};
		const index = buildMappingIndex([ctx]);
		const result = resolve(makeKeyEvent(), index, defaultGuard);
		expect(result?.actionId).toBe('alpha');
	});

	it('pointer bindings can match held keys', () => {
		const ctx: MappingContext = {
			id: 'held-keys',
			priority: 100,
			enabled: true,
			bindings: [
				{ id: 'hk1', pattern: { type: 'drag', heldKeys: { byCode: { Space: true } } }, actionId: 'pan' },
			],
		};
		const index = buildMappingIndex([ctx]);
		const result = resolve(
			makeEvent({ type: 'drag', heldKeys: { byCode: { Space: true }, byKey: { ' ': true } } }),
			index,
			{ ...defaultGuard, heldKeys: { byCode: { Space: true }, byKey: { ' ': true } } },
		);
		expect(result?.actionId).toBe('pan');
	});
});
