/**
 * Grid Layout Algorithm Tests
 *
 * Tests the grid layout algorithm by verifying position assignments.
 * Since useGridLayout is a React hook, we test the core algorithm:
 * sorting, column count, cell sizing, and centering.
 */

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

// --- 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 addNode(store: ReturnType<typeof createStore>, id: string, x = 0, y = 0) {
	store.set(addNodeToLocalGraphAtom, makeDbNode(id, x, y));
}

/**
 * Pure grid layout algorithm extracted from useGridLayout for testing.
 */
function computeGridLayout(
	graph: Graph<GraphNodeAttributes, GraphEdgeAttributes>,
	nodes: Array<{ id: string; position: { x: number; y: number } }>,
	options: { columns?: number; gap?: number } = {},
): Map<string, { x: number; y: number }> {
	const { columns, gap = 80 } = options;

	// Sort nodes by position: top-to-bottom, then left-to-right
	const sorted = [...nodes].sort((a, b) => {
		const ay = a.position?.y ?? 0;
		const by = b.position?.y ?? 0;
		if (Math.abs(ay - by) > 50) return ay - by;
		return (a.position?.x ?? 0) - (b.position?.x ?? 0);
	});

	const cols = columns ?? Math.ceil(Math.sqrt(sorted.length));

	// Compute cell size from max node dimensions
	let maxW = 200;
	let maxH = 100;
	for (const node of sorted) {
		if (graph.hasNode(node.id)) {
			const attrs = graph.getNodeAttributes(node.id);
			maxW = Math.max(maxW, attrs.width || 200);
			maxH = Math.max(maxH, attrs.height || 100);
		}
	}

	const cellW = maxW + gap;
	const cellH = maxH + gap;
	const rows = Math.ceil(sorted.length / cols);

	// Center grid around (0,0)
	const totalW = (cols - 1) * cellW;
	const totalH = (rows - 1) * cellH;
	const offsetX = -totalW / 2;
	const offsetY = -totalH / 2;

	const targets = new Map<string, { x: number; y: number }>();
	for (let i = 0; i < sorted.length; i++) {
		const col = i % cols;
		const row = Math.floor(i / cols);
		targets.set(sorted[i].id, {
			x: Math.round(offsetX + col * cellW),
			y: Math.round(offsetY + row * cellH),
		});
	}

	return targets;
}

// --- Tests ---

describe('grid layout algorithm', () => {
	it('places 4 nodes in a 2x2 grid', () => {
		const store = freshStore();
		addNode(store, 'A', 0, 0);
		addNode(store, 'B', 100, 0);
		addNode(store, 'C', 0, 100);
		addNode(store, 'D', 100, 100);

		const graph = store.get(graphAtom);
		const nodes = [
			{ id: 'A', position: { x: 0, y: 0 } },
			{ id: 'B', position: { x: 100, y: 0 } },
			{ id: 'C', position: { x: 0, y: 100 } },
			{ id: 'D', position: { x: 100, y: 100 } },
		];

		const positions = computeGridLayout(graph, nodes);

		// sqrt(4) = 2 columns
		expect(positions.size).toBe(4);

		// All 4 should have distinct positions
		const posArray = [...positions.values()];
		const unique = new Set(posArray.map((p) => `${p.x},${p.y}`));
		expect(unique.size).toBe(4);
	});

	it('centers grid around (0,0)', () => {
		const store = freshStore();
		addNode(store, 'A', 0, 0);

		const graph = store.get(graphAtom);
		const nodes = [{ id: 'A', position: { x: 0, y: 0 } }];

		const positions = computeGridLayout(graph, nodes);

		// Single node should be at (0, 0) — center of a 1x1 grid
		expect(positions.get('A')!.x).toBe(0);
		expect(positions.get('A')!.y).toBe(0);
	});

	it('respects custom column count', () => {
		const store = freshStore();
		addNode(store, 'A', 0, 0);
		addNode(store, 'B', 100, 0);
		addNode(store, 'C', 200, 0);
		addNode(store, 'D', 300, 0);

		const graph = store.get(graphAtom);
		const nodes = [
			{ id: 'A', position: { x: 0, y: 0 } },
			{ id: 'B', position: { x: 100, y: 0 } },
			{ id: 'C', position: { x: 200, y: 0 } },
			{ id: 'D', position: { x: 300, y: 0 } },
		];

		// Force 4 columns (all in one row)
		const positions = computeGridLayout(graph, nodes, { columns: 4 });

		// All should have the same y
		const ys = [...positions.values()].map((p) => p.y);
		expect(new Set(ys).size).toBe(1);

		// Should have 4 different x values
		const xs = [...positions.values()].map((p) => p.x);
		expect(new Set(xs).size).toBe(4);
	});

	it('sorts nodes by spatial position before gridding', () => {
		const store = freshStore();
		// Nodes placed in reverse order
		addNode(store, 'D', 300, 300);
		addNode(store, 'C', 200, 200);
		addNode(store, 'B', 100, 100);
		addNode(store, 'A', 0, 0);

		const graph = store.get(graphAtom);
		const nodes = [
			{ id: 'D', position: { x: 300, y: 300 } },
			{ id: 'C', position: { x: 200, y: 200 } },
			{ id: 'B', position: { x: 100, y: 100 } },
			{ id: 'A', position: { x: 0, y: 0 } },
		];

		const positions = computeGridLayout(graph, nodes);

		// A (top-left source) should end up at the top-left grid position
		const aPos = positions.get('A')!;
		const dPos = positions.get('D')!;
		expect(aPos.x).toBeLessThanOrEqual(dPos.x);
		expect(aPos.y).toBeLessThanOrEqual(dPos.y);
	});

	it('produces consistent spacing with custom gap', () => {
		const store = freshStore();
		addNode(store, 'A', 0, 0);
		addNode(store, 'B', 100, 0);
		addNode(store, 'C', 0, 100);
		addNode(store, 'D', 100, 100);

		const graph = store.get(graphAtom);
		const nodes = [
			{ id: 'A', position: { x: 0, y: 0 } },
			{ id: 'B', position: { x: 100, y: 0 } },
			{ id: 'C', position: { x: 0, y: 100 } },
			{ id: 'D', position: { x: 100, y: 100 } },
		];

		const gap = 50;
		const positions = computeGridLayout(graph, nodes, { gap });

		// Cell width = 200 (default node width) + 50 gap = 250
		const posArray = [...positions.entries()].sort((a, b) => a[0].localeCompare(b[0]));
		// In a 2x2 grid, the horizontal distance between columns should be cellW
		const row1 = posArray.filter(([_, p]) => p.y === posArray[0][1].y);
		if (row1.length === 2) {
			const diff = Math.abs(row1[1][1].x - row1[0][1].x);
			expect(diff).toBe(250); // 200 + 50
		}
	});
});
