import { describe, it, expect, beforeEach } from 'vitest';
import { createStore } from 'jotai';
import Graph from 'graphology';
import type { GraphNodeAttributes, GraphEdgeAttributes, DBGraphNode } from '../core/types';
import { graphAtom, currentGraphIdAtom, preDragNodeAttributesAtom, draggingNodeIdAtom, graphOptions, graphUpdateVersionAtom } from '../core/graph-store';
import { nodePositionUpdateCounterAtom, updateNodePositionAtom, nodePositionAtomFamily } from '../core/graph-position';
import { startNodeDragAtom, endNodeDragAtom, addNodeToLocalGraphAtom } from '../core/graph-mutations';
import { selectedNodeIdsAtom } from '../core/selection-store';
import { pushHistoryAtom, canUndoAtom, undoAtom, historyStateAtom } from '../core/history-store';
import { getNodeDescendants, groupSelectedNodesAtom } from '../core/group-store';
import { setNodeParentAtom } from '../core/group-store';

// --- Helpers ---

function makeDbNode(id: string, x = 0, y = 0, w = 160, h = 80): DBGraphNode {
	return {
		id,
		graph_id: 'test',
		label: `Node ${id}`,
		node_type: 'default' as const,
		configuration: null,
		data: null,
		ui_properties: { x, y, width: w, height: h, size: 10, zIndex: 0 },
		created_at: '2024-01-01T00:00:00Z',
		updated_at: '2024-01-01T00:00:00Z',
	};
}

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

function addNode(store: ReturnType<typeof createStore>, id: string, x = 0, y = 0, w = 160, h = 80) {
	store.set(addNodeToLocalGraphAtom, makeDbNode(id, x, y, w, h));
}

// --- Tests ---

describe('useNodeDrag atoms', () => {
	describe('startNodeDragAtom', () => {
		it('sets draggingNodeIdAtom correctly', () => {
			const store = freshStore();
			addNode(store, 'a', 100, 200);

			store.set(startNodeDragAtom, { nodeId: 'a' });

			expect(store.get(draggingNodeIdAtom)).toBe('a');
		});

		it('does nothing for non-existent node', () => {
			const store = freshStore();

			store.set(startNodeDragAtom, { nodeId: 'nonexistent' });

			expect(store.get(draggingNodeIdAtom)).toBeNull();
		});
	});

	describe('endNodeDragAtom', () => {
		it('clears draggingNodeIdAtom', () => {
			const store = freshStore();
			addNode(store, 'a', 100, 200);

			store.set(startNodeDragAtom, { nodeId: 'a' });
			expect(store.get(draggingNodeIdAtom)).toBe('a');

			store.set(endNodeDragAtom);
			expect(store.get(draggingNodeIdAtom)).toBeNull();
		});

		it('clears preDragNodeAttributesAtom', () => {
			const store = freshStore();
			addNode(store, 'a', 100, 200);

			store.set(startNodeDragAtom, { nodeId: 'a' });
			expect(store.get(preDragNodeAttributesAtom)).not.toBeNull();

			store.set(endNodeDragAtom);
			expect(store.get(preDragNodeAttributesAtom)).toBeNull();
		});
	});

	describe('position updates via updateNodePositionAtom', () => {
		it('are reflected in nodePositionAtomFamily', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);

			store.set(updateNodePositionAtom, { nodeId: 'a', position: { x: 50, y: 75 } });

			const pos = store.get(nodePositionAtomFamily('a'));
			expect(pos).toEqual({ x: 50, y: 75 });
		});

		it('updates both x and y independently', () => {
			const store = freshStore();
			addNode(store, 'a', 10, 20);

			store.set(updateNodePositionAtom, { nodeId: 'a', position: { x: 300, y: 400 } });

			const pos = store.get(nodePositionAtomFamily('a'));
			expect(pos.x).toBe(300);
			expect(pos.y).toBe(400);
		});
	});

	describe('pushHistoryAtom before drag makes undo available', () => {
		it('canUndoAtom becomes true after pushHistory', () => {
			const store = freshStore();
			addNode(store, 'a', 100, 200);

			expect(store.get(canUndoAtom)).toBe(false);

			store.set(pushHistoryAtom, 'before drag');

			expect(store.get(canUndoAtom)).toBe(true);
		});

		it('undo restores previous state', () => {
			const store = freshStore();
			addNode(store, 'a', 100, 200);

			// Push history before drag
			store.set(pushHistoryAtom, 'before drag');

			// Simulate drag by updating position
			store.set(updateNodePositionAtom, { nodeId: 'a', position: { x: 500, y: 600 } });

			// Undo should be available
			expect(store.get(canUndoAtom)).toBe(true);

			const history = store.get(historyStateAtom);
			expect(history.past.length).toBe(1);
		});
	});

	describe('batch position updates', () => {
		it('increment nodePositionUpdateCounterAtom', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			addNode(store, 'b', 100, 100);

			const before = store.get(nodePositionUpdateCounterAtom);

			store.set(updateNodePositionAtom, { nodeId: 'a', position: { x: 10, y: 10 } });
			store.set(updateNodePositionAtom, { nodeId: 'b', position: { x: 20, y: 20 } });

			const after = store.get(nodePositionUpdateCounterAtom);
			expect(after).toBe(before + 2);
		});
	});

	describe('group drag with getNodeDescendants', () => {
		it('returns all children when dragging a group node', () => {
			const store = freshStore();
			addNode(store, 'group', 0, 0, 400, 300);
			addNode(store, 'child1', 10, 10);
			addNode(store, 'child2', 50, 50);

			store.set(setNodeParentAtom, { nodeId: 'child1', parentId: 'group' });
			store.set(setNodeParentAtom, { nodeId: 'child2', parentId: 'group' });

			const graph = store.get(graphAtom);
			const descendants = getNodeDescendants(graph, 'group');

			expect(descendants).toContain('child1');
			expect(descendants).toContain('child2');
			expect(descendants).toHaveLength(2);
		});

		it('returns nested descendants recursively', () => {
			const store = freshStore();
			addNode(store, 'root', 0, 0, 400, 300);
			addNode(store, 'mid', 10, 10, 200, 150);
			addNode(store, 'leaf', 20, 20);

			store.set(setNodeParentAtom, { nodeId: 'mid', parentId: 'root' });
			store.set(setNodeParentAtom, { nodeId: 'leaf', parentId: 'mid' });

			const graph = store.get(graphAtom);
			const descendants = getNodeDescendants(graph, 'root');

			expect(descendants).toContain('mid');
			expect(descendants).toContain('leaf');
			expect(descendants).toHaveLength(2);
		});

		it('returns empty array for node with no children', () => {
			const store = freshStore();
			addNode(store, 'solo', 0, 0);

			const graph = store.get(graphAtom);
			const descendants = getNodeDescendants(graph, 'solo');

			expect(descendants).toHaveLength(0);
		});
	});

	describe('pre-drag attributes', () => {
		it('are captured on drag start', () => {
			const store = freshStore();
			addNode(store, 'a', 100, 200, 160, 80);

			store.set(startNodeDragAtom, { nodeId: 'a' });

			const preDrag = store.get(preDragNodeAttributesAtom);
			expect(preDrag).not.toBeNull();
			expect(preDrag!.x).toBe(100);
			expect(preDrag!.y).toBe(200);
		});

		it('are a deep copy (not a reference)', () => {
			const store = freshStore();
			addNode(store, 'a', 100, 200);

			store.set(startNodeDragAtom, { nodeId: 'a' });
			const preDrag = store.get(preDragNodeAttributesAtom);

			// Mutate graph position after drag start
			store.set(updateNodePositionAtom, { nodeId: 'a', position: { x: 999, y: 999 } });

			// Pre-drag should still have original values
			expect(preDrag!.x).toBe(100);
			expect(preDrag!.y).toBe(200);
		});
	});
});
