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 { updateNodePositionAtom } from '../core/graph-position';
import { edgeFamilyAtom } from '../core/graph-derived';
import { addNodeToLocalGraphAtom, endNodeDragAtom, startNodeDragAtom } from '../core/graph-mutations';
import { addEdgeToLocalGraphAtom } from '../core/graph-mutations-edges';
import {
	collapsedEdgeRemapAtom,
	autoResizeGroupAtom,
	collapseGroupAtom,
	expandGroupAtom,
	setNodeParentAtom,
} from '../core/group-store';
import { visibleEdgeKeysAtom } from '../core/virtualization-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, w = 200, h = 100): DBGraphNode {
	return {
		id,
		graph_id: 'test-graph',
		label: `Node ${id}`,
		node_type: 'default',
		configuration: null,
		ui_properties: { x, y, width: w, height: h, 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',
	};
}

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('edge-rerouting', () => {
	describe('collapsedEdgeRemapAtom', () => {
		it('returns empty map when no groups are collapsed', () => {
			const store = freshStore();
			addNode(store, 'group');
			addNode(store, 'child');
			store.set(setNodeParentAtom, { nodeId: 'child', parentId: 'group' });

			const remap = store.get(collapsedEdgeRemapAtom);
			expect(remap.size).toBe(0);
		});

		it('maps collapsed child to its group', () => {
			const store = freshStore();
			addNode(store, 'group');
			addNode(store, 'child');
			store.set(setNodeParentAtom, { nodeId: 'child', parentId: 'group' });
			store.set(collapseGroupAtom, 'group');

			const remap = store.get(collapsedEdgeRemapAtom);
			expect(remap.get('child')).toBe('group');
		});

		it('maps to outermost collapsed ancestor for nested groups', () => {
			const store = freshStore();
			addNode(store, 'outer');
			addNode(store, 'inner');
			addNode(store, 'leaf');
			store.set(setNodeParentAtom, { nodeId: 'inner', parentId: 'outer' });
			store.set(setNodeParentAtom, { nodeId: 'leaf', parentId: 'inner' });
			store.set(collapseGroupAtom, 'outer');
			store.set(collapseGroupAtom, 'inner');

			const remap = store.get(collapsedEdgeRemapAtom);
			// Both inner and leaf should map to outer (the outermost collapsed ancestor)
			expect(remap.get('inner')).toBe('outer');
			expect(remap.get('leaf')).toBe('outer');
		});

		it('clears remap when group is expanded', () => {
			const store = freshStore();
			addNode(store, 'group');
			addNode(store, 'child');
			store.set(setNodeParentAtom, { nodeId: 'child', parentId: 'group' });
			store.set(collapseGroupAtom, 'group');
			expect(store.get(collapsedEdgeRemapAtom).get('child')).toBe('group');

			store.set(expandGroupAtom, 'group');
			expect(store.get(collapsedEdgeRemapAtom).size).toBe(0);
		});
	});

	describe('visibleEdgeKeysAtom with collapsed groups', () => {
		it('hides internal edges when group is collapsed', () => {
			const store = freshStore();
			addNode(store, 'group');
			addNode(store, 'a');
			addNode(store, 'b');
			store.set(setNodeParentAtom, { nodeId: 'a', parentId: 'group' });
			store.set(setNodeParentAtom, { nodeId: 'b', parentId: 'group' });
			addEdge(store, 'e1', 'a', 'b');

			// Before collapse: edge is visible
			const before = store.get(visibleEdgeKeysAtom);
			expect(before).toContain('e1');

			// After collapse: internal edge is hidden (both map to same group)
			store.set(collapseGroupAtom, 'group');
			const after = store.get(visibleEdgeKeysAtom);
			expect(after).not.toContain('e1');
		});

		it('keeps cross-boundary edges when group is collapsed', () => {
			const store = freshStore();
			addNode(store, 'group');
			addNode(store, 'child');
			addNode(store, 'external');
			store.set(setNodeParentAtom, { nodeId: 'child', parentId: 'group' });
			addEdge(store, 'e1', 'child', 'external');

			store.set(collapseGroupAtom, 'group');
			const edges = store.get(visibleEdgeKeysAtom);
			// Edge should still be visible (re-routed from child→external to group→external)
			expect(edges).toContain('e1');
		});
	});

	describe('edgeFamilyAtom with collapsed groups', () => {
		it('remaps source position to group node when source is collapsed', () => {
			const store = freshStore();
			addNode(store, 'group', 100, 100);
			addNode(store, 'child', 150, 150);
			addNode(store, 'external', 500, 500);
			store.set(setNodeParentAtom, { nodeId: 'child', parentId: 'group' });
			addEdge(store, 'e1', 'child', 'external');

			store.set(collapseGroupAtom, 'group');

			const edgeState = store.get(edgeFamilyAtom('e1'));
			expect(edgeState).not.toBeNull();
			// Source should be remapped to group node
			expect(edgeState!.sourceId).toBe('group');
			expect(edgeState!.sourcePosition).toEqual({ x: 100, y: 100 });
		});

		it('remaps target position to group node when target is collapsed', () => {
			const store = freshStore();
			addNode(store, 'group', 100, 100);
			addNode(store, 'child', 150, 150);
			addNode(store, 'external', 500, 500);
			store.set(setNodeParentAtom, { nodeId: 'child', parentId: 'group' });
			addEdge(store, 'e1', 'external', 'child');

			store.set(collapseGroupAtom, 'group');

			const edgeState = store.get(edgeFamilyAtom('e1'));
			expect(edgeState).not.toBeNull();
			// Target should be remapped to group node
			expect(edgeState!.targetId).toBe('group');
			expect(edgeState!.targetPosition).toEqual({ x: 100, y: 100 });
		});
	});

	describe('autoResizeGroupAtom', () => {
		it('resizes group to contain children with padding', () => {
			const store = freshStore();
			addNode(store, 'group', 0, 0);
			addNode(store, 'a', 50, 50);
			addNode(store, 'b', 300, 200);
			store.set(setNodeParentAtom, { nodeId: 'a', parentId: 'group' });
			store.set(setNodeParentAtom, { nodeId: 'b', parentId: 'group' });

			store.set(autoResizeGroupAtom, 'group');

			const graph = store.get(graphAtom);
			const attrs = graph.getNodeAttributes('group') as GraphNodeAttributes;
			// Children span x: 50..500 (300+200), y: 50..300 (200+100)
			// With 20px padding and 30px header:
			expect(attrs.x).toBe(30);   // 50 - 20
			expect(attrs.y).toBe(0);    // 50 - 20 - 30
			expect(attrs.width).toBe(490); // (500 - 50) + 40
			expect(attrs.height).toBe(320); // (300 - 50) + 40 + 30
		});

		it('does nothing for nodes with no children', () => {
			const store = freshStore();
			addNode(store, 'lonely', 100, 100);

			store.set(autoResizeGroupAtom, 'lonely');

			const graph = store.get(graphAtom);
			const attrs = graph.getNodeAttributes('lonely') as GraphNodeAttributes;
			expect(attrs.x).toBe(100);
			expect(attrs.y).toBe(100);
		});
	});

	describe('endNodeDragAtom auto-resize', () => {
		it('triggers auto-resize on parent group when drag ends', () => {
			const store = freshStore();
			addNode(store, 'group', 0, 0);
			addNode(store, 'child', 50, 50);
			store.set(setNodeParentAtom, { nodeId: 'child', parentId: 'group' });

			// Start drag
			store.set(startNodeDragAtom, { nodeId: 'child' });
			// Move node
			store.set(updateNodePositionAtom, { nodeId: 'child', position: { x: 400, y: 400 } });
			// End drag
			store.set(endNodeDragAtom);

			const graph = store.get(graphAtom);
			const attrs = graph.getNodeAttributes('group') as GraphNodeAttributes;
			// Group should have resized to contain child at (400, 400)
			expect(attrs.x).toBe(380); // 400 - 20
			expect(attrs.y).toBe(350); // 400 - 20 - 30
		});
	});
});
