import { describe, it, expect } from 'vitest';
import { createStore } from 'jotai';
import Graph from 'graphology';
import type { GraphNodeAttributes, GraphEdgeAttributes, DBGraphNode } from '../core/types';
import { graphAtom, graphOptions, graphUpdateVersionAtom, draggingNodeIdAtom, edgeCreationAtom } from '../core/graph-store';
import { selectedNodeIdsAtom, focusedNodeIdAtom, selectSingleNodeAtom, setFocusedNodeAtom, addNodesToSelectionAtom } from '../core/selection-store';
import { selectedEdgeIdAtom } from '../core/selection-store';
import {
	findNearestNode,
	cycleFocus,
	navigateFocus,
	activateFocusedNode,
	nudgeSelection,
	deleteSelection,
	cutSelection,
	getCurrentSubject,
	isRepeatBlocked,
	getSelectedNodeIds,
	clearSelectionState,
	escapeInput,
	selectAll,
	type Direction,
} from '../gestures/input-action-helpers';
import type { InputEvent, KeyInputEvent, PointerGestureEvent } from '../gestures/types';
import { NO_HELD_KEYS, NO_MODIFIERS } from '../gestures/types';

// =============================================================================
// Helpers
// =============================================================================

function freshStore() {
	const store = createStore();
	store.set(graphAtom, new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions));
	return store;
}

function addNode(store: ReturnType<typeof createStore>, id: string, x: number, y: number, zIndex = 0) {
	const graph = store.get(graphAtom);
	const attrs: GraphNodeAttributes = {
		x, y, size: 15, width: 200, height: 100, color: '#ccc', label: id, zIndex,
		dbData: { id, graph_id: 'test', label: id, node_type: 'default', configuration: null, ui_properties: { x, y }, data: null, created_at: '', updated_at: '' } as DBGraphNode,
	};
	graph.addNode(id, attrs);
	store.set(graphAtom, graph.copy());
	store.set(graphUpdateVersionAtom, (v) => v + 1);
}

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

function makePointerEvent(overrides?: Partial<PointerGestureEvent>): PointerGestureEvent {
	return {
		kind: 'pointer',
		type: 'tap',
		phase: 'instant',
		subject: { kind: 'background' },
		source: 'mouse',
		button: 0,
		modifiers: NO_MODIFIERS,
		heldKeys: NO_HELD_KEYS,
		screenPosition: { x: 0, y: 0 },
		worldPosition: { x: 0, y: 0 },
		...overrides,
	};
}

// =============================================================================
// Tests
// =============================================================================

describe('input-action-helpers', () => {
	describe('isRepeatBlocked', () => {
		it('returns true for key events with repeat', () => {
			expect(isRepeatBlocked(makeKeyEvent({ repeat: true }))).toBe(true);
		});

		it('returns false for key events without repeat', () => {
			expect(isRepeatBlocked(makeKeyEvent({ repeat: false }))).toBe(false);
		});

		it('returns false for pointer events', () => {
			expect(isRepeatBlocked(makePointerEvent())).toBe(false);
		});
	});

	describe('getSelectedNodeIds', () => {
		it('returns empty array when no selection', () => {
			const store = freshStore();
			expect(getSelectedNodeIds(store)).toEqual([]);
		});

		it('returns selected node IDs', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			store.set(addNodesToSelectionAtom, ['a']);
			expect(getSelectedNodeIds(store)).toEqual(['a']);
		});
	});

	describe('getCurrentSubject', () => {
		it('returns background when nothing active', () => {
			const store = freshStore();
			expect(getCurrentSubject(store)).toEqual({ kind: 'background' });
		});

		it('returns node subject when dragging', () => {
			const store = freshStore();
			addNode(store, 'n1', 0, 0);
			store.set(draggingNodeIdAtom, 'n1');
			expect(getCurrentSubject(store)).toEqual({ kind: 'node', nodeId: 'n1' });
		});

		it('returns focused node subject', () => {
			const store = freshStore();
			addNode(store, 'n1', 0, 0);
			store.set(setFocusedNodeAtom, 'n1');
			expect(getCurrentSubject(store)).toEqual({ kind: 'node', nodeId: 'n1' });
		});
	});

	describe('findNearestNode', () => {
		it('returns null for empty graph', () => {
			const store = freshStore();
			expect(findNearestNode(null, store, 'right')).toBe(null);
		});

		it('returns first node (top-left) when no current node', () => {
			const store = freshStore();
			addNode(store, 'a', 100, 200);
			addNode(store, 'b', 0, 0);
			expect(findNearestNode(null, store, 'right')).toBe('b');
		});

		it('finds nearest node to the right', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			addNode(store, 'b', 300, 0);
			addNode(store, 'c', 600, 0);
			expect(findNearestNode('a', store, 'right')).toBe('b');
		});

		it('finds nearest node to the left', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			addNode(store, 'b', 300, 0);
			expect(findNearestNode('b', store, 'left')).toBe('a');
		});

		it('finds nearest node below', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			addNode(store, 'b', 0, 300);
			expect(findNearestNode('a', store, 'down')).toBe('b');
		});

		it('finds nearest node above', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			addNode(store, 'b', 0, 300);
			expect(findNearestNode('b', store, 'up')).toBe('a');
		});

		it('returns null when no node in direction', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			addNode(store, 'b', 300, 0);
			expect(findNearestNode('b', store, 'right')).toBe(null);
		});

		it('prefers closer nodes in the primary direction', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			addNode(store, 'b', 200, 50);  // close right, slightly off axis
			addNode(store, 'c', 500, 0);   // far right, on axis
			expect(findNearestNode('a', store, 'right')).toBe('b');
		});

		it('falls back to first node if current not found', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			expect(findNearestNode('nonexistent', store, 'right')).toBe('a');
		});
	});

	describe('cycleFocus', () => {
		it('does nothing with empty graph', () => {
			const store = freshStore();
			cycleFocus(store, 1);
			expect(store.get(focusedNodeIdAtom)).toBe(null);
		});

		it('cycles forward through nodes by z-index', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0, 0);
			addNode(store, 'b', 100, 0, 1);
			addNode(store, 'c', 200, 0, 2);

			// No focus → focus first (index 0 wraps to 0)
			cycleFocus(store, 1);
			const focused = store.get(focusedNodeIdAtom);
			expect(focused).toBeTruthy();
		});

		it('cycles backward', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0, 0);
			addNode(store, 'b', 100, 0, 1);

			// No focus → last node
			cycleFocus(store, -1);
			expect(store.get(focusedNodeIdAtom)).toBe('b');
		});
	});

	describe('activateFocusedNode', () => {
		it('selects the focused node', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			store.set(setFocusedNodeAtom, 'a');

			activateFocusedNode(store, false);
			expect(store.get(selectedNodeIdsAtom).has('a')).toBe(true);
		});

		it('does nothing when no focusable node', () => {
			const store = freshStore();
			activateFocusedNode(store, false);
			expect(store.get(selectedNodeIdsAtom).size).toBe(0);
		});
	});

	describe('nudgeSelection', () => {
		it('moves selected nodes by dx/dy', () => {
			const store = freshStore();
			addNode(store, 'a', 100, 100);
			store.set(addNodesToSelectionAtom, ['a']);

			nudgeSelection(store, 10, 20, 'Nudge');

			const graph = store.get(graphAtom);
			expect(graph.getNodeAttribute('a', 'x')).toBe(110);
			expect(graph.getNodeAttribute('a', 'y')).toBe(120);
		});

		it('does nothing when no nodes selected', () => {
			const store = freshStore();
			addNode(store, 'a', 100, 100);
			nudgeSelection(store, 10, 20, 'Nudge');

			const graph = store.get(graphAtom);
			expect(graph.getNodeAttribute('a', 'x')).toBe(100);
		});
	});

	describe('deleteSelection', () => {
		it('deletes selected nodes', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			addNode(store, 'b', 100, 0);
			store.set(addNodesToSelectionAtom, ['a']);

			deleteSelection(store);

			const graph = store.get(graphAtom);
			expect(graph.hasNode('a')).toBe(false);
			expect(graph.hasNode('b')).toBe(true);
		});

		it('clears selection after delete', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			store.set(addNodesToSelectionAtom, ['a']);

			deleteSelection(store);
			expect(store.get(selectedNodeIdsAtom).size).toBe(0);
		});
	});

	describe('selectAll', () => {
		it('selects all nodes', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			addNode(store, 'b', 100, 0);
			addNode(store, 'c', 200, 0);

			selectAll(store);

			const selected = store.get(selectedNodeIdsAtom);
			expect(selected.size).toBe(3);
			expect(selected.has('a')).toBe(true);
			expect(selected.has('b')).toBe(true);
			expect(selected.has('c')).toBe(true);
		});
	});

	describe('clearSelectionState', () => {
		it('clears both node and edge selection', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			store.set(addNodesToSelectionAtom, ['a']);

			clearSelectionState(store);
			expect(store.get(selectedNodeIdsAtom).size).toBe(0);
		});
	});
});
