import { describe, it, expect } 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, mergeNodesAtom } from '../core/graph-mutations';
import { addEdgeToLocalGraphAtom } from '../core/graph-mutations-edges';

// --- Helpers ---

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

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: null,
		filter_condition: null,
		ui_properties: null,
		created_at: '2024-01-01T00:00:00Z',
		updated_at: '2024-01-01T00:00:00Z',
	};
}

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

// --- Tests ---

describe('merge-nodes', () => {
	it('removes doomed nodes, survivor remains', () => {
		const store = freshStore();
		addNode(store, 'A', 100, 100);
		addNode(store, 'B', 200, 200);
		addNode(store, 'C', 300, 300);

		store.set(mergeNodesAtom, { nodeIds: ['A', 'B', 'C'] });

		const graph = store.get(graphAtom);
		expect(graph.hasNode('A')).toBe(true);
		expect(graph.hasNode('B')).toBe(false);
		expect(graph.hasNode('C')).toBe(false);
	});

	it('re-routes edges from doomed to survivor', () => {
		const store = freshStore();
		addNode(store, 'A', 100, 100);
		addNode(store, 'B', 200, 200);
		addNode(store, 'ext', 500, 500);
		store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'B', 'ext'));

		store.set(mergeNodesAtom, { nodeIds: ['A', 'B'] });

		const graph = store.get(graphAtom);
		// B is gone, but a new edge from A→ext should exist
		expect(graph.hasNode('B')).toBe(false);
		const aEdges = graph.edges('A');
		expect(aEdges.length).toBeGreaterThanOrEqual(1);
		const hasEdgeToExt = aEdges.some((e) => graph.target(e) === 'ext');
		expect(hasEdgeToExt).toBe(true);
	});

	it('re-routes incoming edges to survivor', () => {
		const store = freshStore();
		addNode(store, 'A', 100, 100);
		addNode(store, 'B', 200, 200);
		addNode(store, 'ext', 500, 500);
		store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'ext', 'B'));

		store.set(mergeNodesAtom, { nodeIds: ['A', 'B'] });

		const graph = store.get(graphAtom);
		const aEdges = graph.edges('A');
		const hasEdgeFromExt = aEdges.some((e) => graph.source(e) === 'ext');
		expect(hasEdgeFromExt).toBe(true);
	});

	it('discards edges between merged nodes (would be self-loops)', () => {
		const store = freshStore();
		addNode(store, 'A', 100, 100);
		addNode(store, 'B', 200, 200);
		store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'A', 'B'));

		store.set(mergeNodesAtom, { nodeIds: ['A', 'B'] });

		const graph = store.get(graphAtom);
		// No edges should remain (A→B would become A→A, which is discarded)
		expect(graph.edges('A').length).toBe(0);
	});

	it('handles merging with edges in both directions', () => {
		const store = freshStore();
		addNode(store, 'A', 100, 100);
		addNode(store, 'B', 200, 200);
		addNode(store, 'ext1', 400, 100);
		addNode(store, 'ext2', 400, 300);
		store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'B', 'ext1'));
		store.set(addEdgeToLocalGraphAtom, makeDbEdge('e2', 'ext2', 'B'));
		store.set(addEdgeToLocalGraphAtom, makeDbEdge('e3', 'A', 'B'));

		store.set(mergeNodesAtom, { nodeIds: ['A', 'B'] });

		const graph = store.get(graphAtom);
		expect(graph.hasNode('B')).toBe(false);
		const aEdges = graph.edges('A');
		// Should have edge to ext1 and edge from ext2, but NOT A→A
		const targets = aEdges.filter((e) => graph.source(e) === 'A').map((e) => graph.target(e));
		const sources = aEdges.filter((e) => graph.target(e) === 'A').map((e) => graph.source(e));
		expect(targets).toContain('ext1');
		expect(sources).toContain('ext2');
		// No self-loops
		expect(targets).not.toContain('A');
	});

	it('no-ops with fewer than 2 nodes', () => {
		const store = freshStore();
		addNode(store, 'A', 100, 100);

		store.set(mergeNodesAtom, { nodeIds: ['A'] });

		const graph = store.get(graphAtom);
		expect(graph.hasNode('A')).toBe(true);
		expect(graph.order).toBe(1);
	});

	it('no-ops when survivor does not exist', () => {
		const store = freshStore();
		addNode(store, 'A', 100, 100);

		store.set(mergeNodesAtom, { nodeIds: ['nonexistent', 'A'] });

		const graph = store.get(graphAtom);
		// A should still exist since nonexistent (survivor) wasn't found
		expect(graph.hasNode('A')).toBe(true);
	});
});
