import { describe, it, expect, beforeEach } from 'vitest';
import { createStore } from 'jotai';
import Graph from 'graphology';
import type { GraphNodeAttributes, GraphEdgeAttributes, DBGraphNode, DBGraphEdge } from '../core/types';
import { graphAtom, graphOptions, currentGraphIdAtom } from '../core/graph-store';
import {
	addNodeToLocalGraphAtom,
	optimisticDeleteNodeAtom,
	optimisticDeleteEdgeAtom,
	loadGraphFromDbAtom,
	startNodeDragAtom,
	endNodeDragAtom,
} from '../core/graph-mutations';
import { addEdgeToLocalGraphAtom } from '../core/graph-mutations-edges';

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

function makeDbEdge(id: string, source: string, target: string): 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',
	};
}

describe('graph-mutations', () => {
	let store: ReturnType<typeof createStore>;

	beforeEach(() => {
		store = createStore();
		store.set(graphAtom, new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions));
		store.set(currentGraphIdAtom, 'test-graph');
	});

	describe('addNodeToLocalGraphAtom', () => {
		it('adds a node to the graph', () => {
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', 100, 200));
			const graph = store.get(graphAtom);
			expect(graph.hasNode('n1')).toBe(true);
			expect(graph.order).toBe(1);
		});

		it('sets position from ui_properties', () => {
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', 150, 250));
			const graph = store.get(graphAtom);
			const attrs = graph.getNodeAttributes('n1') as GraphNodeAttributes;
			expect(attrs.x).toBe(150);
			expect(attrs.y).toBe(250);
		});

		it('skips duplicate node IDs', () => {
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			expect(store.get(graphAtom).order).toBe(1);
		});
	});

	describe('optimisticDeleteNodeAtom', () => {
		it('removes a node from the graph', () => {
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			expect(store.get(graphAtom).order).toBe(1);
			store.set(optimisticDeleteNodeAtom, { nodeId: 'n1' });
			expect(store.get(graphAtom).order).toBe(0);
		});

		it('is a no-op for non-existent nodes', () => {
			store.set(optimisticDeleteNodeAtom, { nodeId: 'nonexistent' });
			expect(store.get(graphAtom).order).toBe(0);
		});
	});

	describe('optimisticDeleteEdgeAtom', () => {
		it('removes an edge from the graph', () => {
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n2'));
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));
			expect(store.get(graphAtom).size).toBe(1);
			store.set(optimisticDeleteEdgeAtom, { edgeKey: 'e1' });
			expect(store.get(graphAtom).size).toBe(0);
		});
	});

	describe('loadGraphFromDbAtom', () => {
		it('populates graph from DB data', () => {
			const nodes = [makeDbNode('n1'), makeDbNode('n2')];
			const edges = [makeDbEdge('e1', 'n1', 'n2')];
			store.set(loadGraphFromDbAtom, nodes, edges);
			const graph = store.get(graphAtom);
			expect(graph.order).toBe(2);
			expect(graph.size).toBe(1);
		});

		it('removes nodes not in fetched data', () => {
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n2'));
			store.set(loadGraphFromDbAtom, [makeDbNode('n1')], []);
			expect(store.get(graphAtom).order).toBe(1);
			expect(store.get(graphAtom).hasNode('n1')).toBe(true);
		});

		it('skips edges with missing endpoints', () => {
			store.set(loadGraphFromDbAtom, [makeDbNode('n1')], [makeDbEdge('e1', 'n1', 'missing')]);
			expect(store.get(graphAtom).size).toBe(0);
		});
	});

	describe('drag lifecycle', () => {
		it('startNodeDrag / endNodeDrag round-trip', () => {
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1'));
			store.set(startNodeDragAtom, { nodeId: 'n1' });
			store.set(endNodeDragAtom);
		});
	});
});
