import { describe, it, expect } from 'vitest';
import { createStore } from 'jotai';
import Graph from 'graphology';
import type { DBGraphNode, DBGraphEdge, GraphNodeAttributes, GraphEdgeAttributes } from '../core/types';
import { graphAtom, currentGraphIdAtom, graphUpdateVersionAtom, graphOptions, draggingNodeIdAtom, edgeCreationAtom } from '../core/graph-store';
import { nodePositionAtomFamily, nodePositionUpdateCounterAtom, updateNodePositionAtom, cleanupNodePositionAtom, cleanupAllNodePositionsAtom } from '../core/graph-position';
import { nodeKeysAtom, edgeKeysAtom, highestZIndexAtom, nodeFamilyAtom, edgeFamilyAtom, uiNodesAtom, edgeKeysWithTempEdgeAtom } from '../core/graph-derived';
import { addNodeToLocalGraphAtom, optimisticDeleteNodeAtom, optimisticDeleteEdgeAtom, swapEdgeAtomicAtom, loadGraphFromDbAtom, startNodeDragAtom, endNodeDragAtom } from '../core/graph-mutations';
import { addEdgeToLocalGraphAtom, removeEdgeFromLocalGraphAtom } from '../core/graph-mutations-edges';

// --- Helpers ---

function makeDbNode(id: string, overrides: Partial<DBGraphNode> = {}): DBGraphNode {
	return {
		id,
		graph_id: 'test-graph',
		label: `Node ${id}`,
		node_type: 'default',
		configuration: null,
		ui_properties: { x: 100, y: 200, width: 300, height: 150, size: 15, zIndex: 0 },
		data: null,
		created_at: '2024-01-01T00:00:00Z',
		updated_at: '2024-01-01T00:00:00Z',
		...overrides,
	};
}

function makeDbEdge(id: string, source: string, target: string, overrides: Partial<DBGraphEdge> = {}): DBGraphEdge {
	return {
		id,
		graph_id: 'test-graph',
		source_node_id: source,
		target_node_id: target,
		edge_type: 'default',
		filter_condition: null,
		ui_properties: null,
		data: null,
		created_at: '2024-01-01T00:00:00Z',
		updated_at: '2024-01-01T00:00:00Z',
		...overrides,
	};
}

/** Create an isolated store with a fresh graph (avoids shared mutable state) */
function freshStore(): ReturnType<typeof createStore> {
	const store = createStore();
	store.set(graphAtom, new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions));
	store.set(currentGraphIdAtom, 'test-graph');
	return store;
}

function setupStoreWithNodes(nodeIds: string[]): ReturnType<typeof createStore> {
	const store = freshStore();
	for (const id of nodeIds) {
		store.set(addNodeToLocalGraphAtom, makeDbNode(id));
	}
	return store;
}

// =============================================================================
// Graph Store Tests
// =============================================================================

describe('graph-store', () => {
	// --- Node Operations ---

	describe('addNodeToLocalGraphAtom', () => {
		it('adds a node to the graph', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));

			const keys = store.get(nodeKeysAtom);
			expect(keys).toContain('n1');
		});

		it('uses ui_properties for position', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', {
				ui_properties: { x: 42, y: 99, width: 200, height: 100 },
			}));

			const pos = store.get(nodePositionAtomFamily('n1'));
			expect(pos.x).toBe(42);
			expect(pos.y).toBe(99);
		});

		it('skips duplicate node IDs', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			const v1 = store.get(graphUpdateVersionAtom);

			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			const v2 = store.get(graphUpdateVersionAtom);

			expect(v2).toBe(v1); // No version bump on skip
		});

		it('increments graphUpdateVersion on add', () => {
			const store = freshStore();
			const v0 = store.get(graphUpdateVersionAtom);
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			expect(store.get(graphUpdateVersionAtom)).toBe(v0 + 1);
		});

		it('sets defaults when ui_properties missing', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', { ui_properties: null }));

			const graph = store.get(graphAtom);
			const attrs = graph.getNodeAttributes('n1') as GraphNodeAttributes;
			expect(attrs.size).toBe(15);
			expect(attrs.color).toBe('#ccc');
		});
	});

	describe('optimisticDeleteNodeAtom', () => {
		it('removes a node from graph', () => {
			const store = setupStoreWithNodes(['n1', 'n2']);
			store.set(optimisticDeleteNodeAtom, { nodeId: 'n1' });

			expect(store.get(nodeKeysAtom)).not.toContain('n1');
			expect(store.get(nodeKeysAtom)).toContain('n2');
		});

		it('also removes connected edges', () => {
			const store = setupStoreWithNodes(['n1', 'n2']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));
			expect(store.get(edgeKeysAtom)).toContain('e1');

			store.set(optimisticDeleteNodeAtom, { nodeId: 'n1' });
			expect(store.get(edgeKeysAtom)).not.toContain('e1');
		});

		it('does nothing for non-existent node', () => {
			const store = setupStoreWithNodes(['n1']);
			const v = store.get(graphUpdateVersionAtom);
			store.set(optimisticDeleteNodeAtom, { nodeId: 'nonexistent' });
			// Version should not change since no deletion occurred
			// (Actually the atom does graph.copy() so version could change, but let's check node count)
			expect(store.get(nodeKeysAtom)).toContain('n1');
		});
	});

	// --- Edge Operations ---

	describe('', () => {
		it('adds an edge between existing nodes', () => {
			const store = setupStoreWithNodes(['n1', 'n2']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));
			expect(store.get(edgeKeysAtom)).toContain('e1');
		});

		it('ignores edge if source node missing', () => {
			const store = setupStoreWithNodes(['n2']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));
			expect(store.get(edgeKeysAtom)).not.toContain('e1');
		});

		it('ignores edge if target node missing', () => {
			const store = setupStoreWithNodes(['n1']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));
			expect(store.get(edgeKeysAtom)).not.toContain('e1');
		});

		it('skips duplicate edge IDs', () => {
			const store = setupStoreWithNodes(['n1', 'n2']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));
			const v1 = store.get(graphUpdateVersionAtom);

			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));
			const v2 = store.get(graphUpdateVersionAtom);
			expect(v2).toBe(v1);
		});
	});

	describe('', () => {
		it('removes an edge', () => {
			const store = setupStoreWithNodes(['n1', 'n2']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));
			store.set(removeEdgeFromLocalGraphAtom, 'e1');
			expect(store.get(edgeKeysAtom)).not.toContain('e1');
		});

		it('does nothing for non-existent edge', () => {
			const store = setupStoreWithNodes(['n1', 'n2']);
			const v = store.get(graphUpdateVersionAtom);
			store.set(removeEdgeFromLocalGraphAtom, 'nonexistent');
			expect(store.get(graphUpdateVersionAtom)).toBe(v);
		});
	});

	describe('optimisticDeleteEdgeAtom', () => {
		it('removes an edge optimistically', () => {
			const store = setupStoreWithNodes(['n1', 'n2']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));
			store.set(optimisticDeleteEdgeAtom, { edgeKey: 'e1' });
			expect(store.get(edgeKeysAtom)).not.toContain('e1');
		});
	});

	describe('swapEdgeAtomicAtom', () => {
		it('replaces temp edge with real edge atomically', () => {
			const store = setupStoreWithNodes(['n1', 'n2']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('temp-edge', 'n1', 'n2'));
			expect(store.get(edgeKeysAtom)).toContain('temp-edge');

			store.set(swapEdgeAtomicAtom, {
				tempEdgeId: 'temp-edge',
				newEdge: makeDbEdge('real-edge', 'n1', 'n2'),
			});

			expect(store.get(edgeKeysAtom)).not.toContain('temp-edge');
			expect(store.get(edgeKeysAtom)).toContain('real-edge');
		});
	});

	// --- Position Atoms ---

	describe('nodePositionAtomFamily', () => {
		it('returns position from graph attributes', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', {
				ui_properties: { x: 50, y: 75 },
			}));

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

		it('returns {0,0} for non-existent node', () => {
			const store = freshStore();
			const pos = store.get(nodePositionAtomFamily('nonexistent'));
			expect(pos).toEqual({ x: 0, y: 0 });
		});

		it('returns same reference for unchanged position (structural equality)', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', {
				ui_properties: { x: 10, y: 20 },
			}));

			const pos1 = store.get(nodePositionAtomFamily('n1'));
			// Bump counter without changing position
			store.set(nodePositionUpdateCounterAtom, (c) => c + 1);
			const pos2 = store.get(nodePositionAtomFamily('n1'));

			expect(pos1).toBe(pos2); // Same reference
		});

		it('returns new reference when position changes', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', {
				ui_properties: { x: 10, y: 20 },
			}));

			const pos1 = store.get(nodePositionAtomFamily('n1'));
			store.set(updateNodePositionAtom, { nodeId: 'n1', position: { x: 30, y: 40 } });
			const pos2 = store.get(nodePositionAtomFamily('n1'));

			expect(pos1).not.toBe(pos2);
			expect(pos2).toEqual({ x: 30, y: 40 });
		});
	});

	describe('updateNodePositionAtom', () => {
		it('updates position in graph', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', {
				ui_properties: { x: 0, y: 0 },
			}));

			store.set(updateNodePositionAtom, { nodeId: 'n1', position: { x: 100, y: 200 } });

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

		it('bumps nodePositionUpdateCounter', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			const c0 = store.get(nodePositionUpdateCounterAtom);

			store.set(updateNodePositionAtom, { nodeId: 'n1', position: { x: 1, y: 1 } });
			expect(store.get(nodePositionUpdateCounterAtom)).toBe(c0 + 1);
		});

		it('ignores non-existent node', () => {
			const store = freshStore();
			const c0 = store.get(nodePositionUpdateCounterAtom);
			store.set(updateNodePositionAtom, { nodeId: 'nope', position: { x: 1, y: 1 } });
			expect(store.get(nodePositionUpdateCounterAtom)).toBe(c0);
		});
	});

	describe('cleanupNodePositionAtom', () => {
		it('removes position atom for node', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			// Atom was created
			store.get(nodePositionAtomFamily('n1'));
			// Cleanup
			store.set(cleanupNodePositionAtom, 'n1');
			// Should not throw, returns zero for non-existent
		});
	});

	// --- Derived Atoms ---

	describe('nodeKeysAtom', () => {
		it('returns all node IDs', () => {
			const store = setupStoreWithNodes(['a', 'b', 'c']);
			const keys = store.get(nodeKeysAtom);
			expect(keys).toHaveLength(3);
			expect(keys).toContain('a');
			expect(keys).toContain('b');
			expect(keys).toContain('c');
		});

		it('returns empty for empty graph', () => {
			const store = freshStore();
			expect(store.get(nodeKeysAtom)).toHaveLength(0);
		});
	});

	describe('edgeKeysAtom', () => {
		it('returns all edge IDs', () => {
			const store = setupStoreWithNodes(['n1', 'n2', 'n3']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e2', 'n2', 'n3'));

			const keys = store.get(edgeKeysAtom);
			expect(keys).toHaveLength(2);
			expect(keys).toContain('e1');
			expect(keys).toContain('e2');
		});
	});

	describe('edgeKeysWithTempEdgeAtom', () => {
		it('includes temp edge when creating', () => {
			const store = setupStoreWithNodes(['n1', 'n2']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));

			store.set(edgeCreationAtom, {
				isCreating: true,
				sourceNodeId: 'n1',
				sourceNodePosition: { x: 0, y: 0 },
				targetPosition: { x: 100, y: 100 },
				hoveredTargetNodeId: null,
				sourceHandle: null,
				targetHandle: null,
				sourcePort: null,
				targetPort: null,
				snappedTargetPosition: null,
			});

			const keys = store.get(edgeKeysWithTempEdgeAtom);
			expect(keys).toContain('temp-creating-edge');
			expect(keys).toContain('e1');
		});

		it('excludes temp edge when not creating', () => {
			const store = setupStoreWithNodes(['n1', 'n2']);
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));

			const keys = store.get(edgeKeysWithTempEdgeAtom);
			expect(keys).not.toContain('temp-creating-edge');
		});
	});

	describe('highestZIndexAtom', () => {
		it('returns highest zIndex', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', {
				ui_properties: { x: 0, y: 0, zIndex: 5 },
			}));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n2', {
				ui_properties: { x: 0, y: 0, zIndex: 10 },
			}));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n3', {
				ui_properties: { x: 0, y: 0, zIndex: 3 },
			}));

			expect(store.get(highestZIndexAtom)).toBe(10);
		});

		it('returns 0 for empty graph', () => {
			const store = freshStore();
			expect(store.get(highestZIndexAtom)).toBe(0);
		});
	});

	describe('uiNodesAtom', () => {
		it('returns UI node state for all nodes', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', {
				ui_properties: { x: 10, y: 20 },
			}));

			const nodes = store.get(uiNodesAtom);
			expect(nodes).toHaveLength(1);
			expect(nodes[0].id).toBe('n1');
			expect(nodes[0].position).toEqual({ x: 10, y: 20 });
			expect(nodes[0].isDragging).toBe(false);
		});
	});

	describe('nodeFamilyAtom', () => {
		it('returns null for non-existent node', () => {
			const store = freshStore();
			expect(store.get(nodeFamilyAtom('nonexistent'))).toBeNull();
		});

		it('returns node state for existing node', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', {
				label: 'Hello',
				ui_properties: { x: 5, y: 10, width: 200, height: 100 },
			}));

			const node = store.get(nodeFamilyAtom('n1'));
			expect(node).not.toBeNull();
			expect(node!.id).toBe('n1');
			expect(node!.width).toBe(200);
			expect(node!.height).toBe(100);
		});
	});

	describe('edgeFamilyAtom', () => {
		it('returns null for non-existent edge', () => {
			const store = freshStore();
			expect(store.get(edgeFamilyAtom('nonexistent'))).toBeNull();
		});

		it('returns edge state with source/target positions', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', { ui_properties: { x: 0, y: 0 } }));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n2', { ui_properties: { x: 100, y: 100 } }));
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));

			const edge = store.get(edgeFamilyAtom('e1'));
			expect(edge).not.toBeNull();
			expect(edge!.sourceId).toBe('n1');
			expect(edge!.targetId).toBe('n2');
			expect(edge!.sourcePosition).toEqual({ x: 0, y: 0 });
			expect(edge!.targetPosition).toEqual({ x: 100, y: 100 });
		});

		it('does not recompute when unrelated node moves (optimization)', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', { ui_properties: { x: 0, y: 0 } }));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n2', { ui_properties: { x: 100, y: 0 } }));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n3', { ui_properties: { x: 200, y: 0 } }));
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e-12', 'n1', 'n2'));

			const edge1 = store.get(edgeFamilyAtom('e-12'));

			// Move n3 (unrelated to edge e-12)
			store.set(updateNodePositionAtom, { nodeId: 'n3', position: { x: 999, y: 999 } });

			const edge2 = store.get(edgeFamilyAtom('e-12'));

			// Edge state should be the same reference since source/target positions unchanged
			// (structural equality on position atoms + atomFamily equality check)
			expect(edge1!.sourcePosition).toBe(edge2!.sourcePosition);
			expect(edge1!.targetPosition).toBe(edge2!.targetPosition);
		});
	});

	// --- Drag State ---

	describe('drag atoms', () => {
		it('startNodeDragAtom sets dragging state and bumps zIndex', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', { ui_properties: { x: 0, y: 0, zIndex: 0 } }));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n2', { ui_properties: { x: 0, y: 0, zIndex: 5 } }));

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

			expect(store.get(draggingNodeIdAtom)).toBe('n1');
			// zIndex should be bumped above n2's zIndex of 5
			const graph = store.get(graphAtom);
			expect(graph.getNodeAttribute('n1', 'zIndex')).toBe(6);
		});

		it('endNodeDragAtom clears dragging state', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			store.set(startNodeDragAtom, { nodeId: 'n1' });
			expect(store.get(draggingNodeIdAtom)).toBe('n1');

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

	// --- Load from DB ---

	describe('loadGraphFromDbAtom', () => {
		it('loads nodes and edges from DB data', () => {
			const store = freshStore();
			store.set(currentGraphIdAtom, 'test-graph');

			const nodes = [makeDbNode('n1'), makeDbNode('n2')];
			const edges = [makeDbEdge('e1', 'n1', 'n2')];

			store.set(loadGraphFromDbAtom, nodes, edges);

			expect(store.get(nodeKeysAtom)).toHaveLength(2);
			expect(store.get(edgeKeysAtom)).toHaveLength(1);
		});

		it('skips sync during drag', () => {
			const store = freshStore();
			store.set(currentGraphIdAtom, 'test-graph');

			// Add initial data
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			store.set(startNodeDragAtom, { nodeId: 'n1' });

			const v = store.get(graphUpdateVersionAtom);
			store.set(loadGraphFromDbAtom, [makeDbNode('n1'), makeDbNode('n2')], []);
			// Should skip — n2 should NOT be added during drag
			expect(store.get(nodeKeysAtom)).not.toContain('n2');
		});

		it('removes nodes deleted from DB', () => {
			const store = freshStore();
			store.set(currentGraphIdAtom, 'test-graph');

			store.set(loadGraphFromDbAtom, [makeDbNode('n1'), makeDbNode('n2')], []);
			expect(store.get(nodeKeysAtom)).toHaveLength(2);

			// Second load with n2 removed
			store.set(loadGraphFromDbAtom, [makeDbNode('n1')], []);
			expect(store.get(nodeKeysAtom)).toHaveLength(1);
			expect(store.get(nodeKeysAtom)).toContain('n1');
		});
	});
});
