import { describe, it, expect } from 'vitest';
import { createStore } from 'jotai';
import Graph from 'graphology';
import type { GraphNodeAttributes, GraphEdgeAttributes, DBGraphNode } from '../core/types';
import { graphAtom, currentGraphIdAtom, graphOptions } from '../core/graph-store';
import { nodePositionUpdateCounterAtom, updateNodePositionAtom, nodePositionAtomFamily } from '../core/graph-position';
import { addNodeToLocalGraphAtom } from '../core/graph-mutations';
import { checkNodesOverlap } from '../utils/layout';

// --- Helpers ---

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

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

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

// --- Tests ---

describe('useForceLayout atoms', () => {
	describe('updateNodePositionAtom updates graph positions correctly', () => {
		it('updates a single node position', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);

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

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

		it('updates multiple node positions', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);
			addNode(store, 'b', 0, 0);
			addNode(store, 'c', 0, 0);

			store.set(updateNodePositionAtom, { nodeId: 'a', position: { x: 100, y: 0 } });
			store.set(updateNodePositionAtom, { nodeId: 'b', position: { x: 400, y: 0 } });
			store.set(updateNodePositionAtom, { nodeId: 'c', position: { x: 700, y: 0 } });

			const posA = store.get(nodePositionAtomFamily('a'));
			const posB = store.get(nodePositionAtomFamily('b'));
			const posC = store.get(nodePositionAtomFamily('c'));

			expect(posA).toEqual({ x: 100, y: 0 });
			expect(posB).toEqual({ x: 400, y: 0 });
			expect(posC).toEqual({ x: 700, y: 0 });
		});
	});

	describe('non-overlapping positions after layout', () => {
		it('multiple nodes get non-overlapping positions when spread apart', () => {
			const store = freshStore();
			const w = 200;
			const h = 100;

			// Place nodes far enough apart that they do not overlap
			addNode(store, 'a', 0, 0, w, h);
			addNode(store, 'b', 300, 0, w, h);
			addNode(store, 'c', 600, 0, w, h);

			const graph = store.get(graphAtom);
			const nodeA = { x: graph.getNodeAttribute('a', 'x'), y: graph.getNodeAttribute('a', 'y'), width: w, height: h };
			const nodeB = { x: graph.getNodeAttribute('b', 'x'), y: graph.getNodeAttribute('b', 'y'), width: w, height: h };
			const nodeC = { x: graph.getNodeAttribute('c', 'x'), y: graph.getNodeAttribute('c', 'y'), width: w, height: h };

			expect(checkNodesOverlap(nodeA, nodeB)).toBe(false);
			expect(checkNodesOverlap(nodeB, nodeC)).toBe(false);
			expect(checkNodesOverlap(nodeA, nodeC)).toBe(false);
		});
	});

	describe('position updates increment nodePositionUpdateCounterAtom', () => {
		it('counter increments on each position update', () => {
			const store = freshStore();
			addNode(store, 'a', 0, 0);

			const before = store.get(nodePositionUpdateCounterAtom);

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

			store.set(updateNodePositionAtom, { nodeId: 'a', position: { x: 20, y: 20 } });
			expect(store.get(nodePositionUpdateCounterAtom)).toBe(before + 2);

			store.set(updateNodePositionAtom, { nodeId: 'a', position: { x: 30, y: 30 } });
			expect(store.get(nodePositionUpdateCounterAtom)).toBe(before + 3);
		});
	});

	describe('checkNodesOverlap utility', () => {
		it('detects two overlapping nodes', () => {
			const node1 = { x: 0, y: 0, width: 200, height: 100 };
			const node2 = { x: 100, y: 50, width: 200, height: 100 };

			expect(checkNodesOverlap(node1, node2)).toBe(true);
		});

		it('detects non-overlapping nodes', () => {
			const node1 = { x: 0, y: 0, width: 200, height: 100 };
			const node2 = { x: 500, y: 500, width: 200, height: 100 };

			expect(checkNodesOverlap(node1, node2)).toBe(false);
		});

		it('detects nodes that share an edge (touching) as not overlapping', () => {
			// Exactly touching: node2 starts where node1 ends
			const node1 = { x: 0, y: 0, width: 200, height: 100 };
			const node2 = { x: 200, y: 0, width: 200, height: 100 };

			expect(checkNodesOverlap(node1, node2)).toBe(false);
		});

		it('after simulated spread, nodes do not overlap', () => {
			// Start overlapping
			const startNodes = [
				{ x: 0, y: 0, width: 200, height: 100 },
				{ x: 50, y: 25, width: 200, height: 100 },
				{ x: 100, y: 50, width: 200, height: 100 },
			];

			// Verify they start overlapping
			expect(checkNodesOverlap(startNodes[0], startNodes[1])).toBe(true);
			expect(checkNodesOverlap(startNodes[1], startNodes[2])).toBe(true);

			// Simulate spread: move them far apart
			const spreadNodes = [
				{ x: 0, y: 0, width: 200, height: 100 },
				{ x: 300, y: 0, width: 200, height: 100 },
				{ x: 600, y: 0, width: 200, height: 100 },
			];

			// Verify no overlaps after spread
			expect(checkNodesOverlap(spreadNodes[0], spreadNodes[1])).toBe(false);
			expect(checkNodesOverlap(spreadNodes[1], spreadNodes[2])).toBe(false);
			expect(checkNodesOverlap(spreadNodes[0], spreadNodes[2])).toBe(false);
		});
	});
});
