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, splitNodeAtom, mergeNodesAtom } from '../core/graph-mutations';
import { addEdgeToLocalGraphAtom } from '../core/graph-mutations-edges';
import { nestNodesOnDropAtom, groupSelectedNodesAtom, ungroupNodesAtom } from '../core/group-store';
import { undoAtom, historyStateAtom } from '../core/history-store';

// --- 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));
}

function addEdge(store: ReturnType<typeof createStore>, id: string, source: string, target: string) {
	store.set(addEdgeToLocalGraphAtom, makeDbEdge(id, source, target));
}

// --- Tests ---

describe('undo split node', () => {
	it('restores original node and removes clone on undo', () => {
		const store = freshStore();
		addNode(store, 'A', 100, 100);
		addEdge(store, 'e1', 'A', 'A'); // self-edge for edge dup test

		const graphBefore = store.get(graphAtom);
		expect(graphBefore.order).toBe(1);

		store.set(splitNodeAtom, {
			nodeId: 'A',
			position1: { x: 50, y: 100 },
			position2: { x: 200, y: 100 },
		});

		const graphAfterSplit = store.get(graphAtom);
		expect(graphAfterSplit.order).toBe(2);

		// Undo
		store.set(undoAtom);

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

	it('pushes history with label "Split node"', () => {
		const store = freshStore();
		addNode(store, 'A', 100, 100);

		store.set(splitNodeAtom, {
			nodeId: 'A',
			position1: { x: 50, y: 100 },
			position2: { x: 200, y: 100 },
		});

		const state = store.get(historyStateAtom);
		expect(state.past.length).toBeGreaterThan(0);
		expect(state.past[state.past.length - 1].label).toBe('Split node');
	});
});

describe('undo merge nodes', () => {
	it('restores all nodes and edges on undo', () => {
		const store = freshStore();
		addNode(store, 'A', 0, 0);
		addNode(store, 'B', 200, 0);
		addNode(store, 'C', 400, 0);
		addEdge(store, 'e1', 'B', 'C');

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

		const graphAfterMerge = store.get(graphAtom);
		expect(graphAfterMerge.hasNode('B')).toBe(false);

		// Undo
		store.set(undoAtom);

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

	it('pushes history with label "Merge N nodes"', () => {
		const store = freshStore();
		addNode(store, 'A');
		addNode(store, 'B');

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

		const state = store.get(historyStateAtom);
		expect(state.past[state.past.length - 1].label).toBe('Merge 2 nodes');
	});
});

describe('undo nest nodes', () => {
	it('restores parentId on undo', () => {
		const store = freshStore();
		addNode(store, 'parent', 0, 0);
		addNode(store, 'child', 100, 100);

		const graphBefore = store.get(graphAtom);
		expect(graphBefore.getNodeAttribute('child', 'parentId')).toBeUndefined();

		store.set(nestNodesOnDropAtom, { nodeIds: ['child'], targetId: 'parent' });

		const graphAfterNest = store.get(graphAtom);
		expect(graphAfterNest.getNodeAttribute('child', 'parentId')).toBe('parent');

		// Undo
		store.set(undoAtom);

		const graphAfterUndo = store.get(graphAtom);
		expect(graphAfterUndo.getNodeAttribute('child', 'parentId')).toBeUndefined();
	});

	it('pushes history with label "Nest nodes"', () => {
		const store = freshStore();
		addNode(store, 'parent');
		addNode(store, 'child');

		store.set(nestNodesOnDropAtom, { nodeIds: ['child'], targetId: 'parent' });

		const state = store.get(historyStateAtom);
		expect(state.past[state.past.length - 1].label).toBe('Nest nodes');
	});
});

describe('undo group/ungroup', () => {
	it('pushes history with label for group', () => {
		const store = freshStore();
		addNode(store, 'A');
		addNode(store, 'B');
		addNode(store, 'group1');

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

		const state = store.get(historyStateAtom);
		expect(state.past[state.past.length - 1].label).toBe('Group 2 nodes');
	});

	it('pushes history with label for ungroup', () => {
		const store = freshStore();
		addNode(store, 'A');
		addNode(store, 'group1');

		const graph = store.get(graphAtom);
		graph.setNodeAttribute('A', 'parentId', 'group1');

		store.set(ungroupNodesAtom, 'group1');

		const state = store.get(historyStateAtom);
		expect(state.past[state.past.length - 1].label).toBe('Ungroup nodes');
	});
});
