/**
 * Tree Layout Algorithm Tests
 *
 * Tests the tree layout algorithm by verifying position assignments
 * on graph structures. Since useTreeLayout is a React hook, we test
 * the core algorithm logic: BFS depth assignment, root detection,
 * and position computation.
 */

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

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

/**
 * Pure tree layout algorithm extracted from useTreeLayout for testing.
 * Returns target positions for each node.
 */
function computeTreeLayout(
	graph: Graph<GraphNodeAttributes, GraphEdgeAttributes>,
	nodeIds: string[],
	options: { direction?: 'top-down' | 'left-right'; levelGap?: number; nodeGap?: number } = {},
): Map<string, { x: number; y: number }> {
	const { direction = 'top-down', levelGap = 200, nodeGap = 100 } = options;

	const nodeIdSet = new Set(nodeIds);
	const children = new Map<string, string[]>();
	const hasIncoming = new Set<string>();

	for (const nodeId of nodeIdSet) {
		children.set(nodeId, []);
	}

	graph.forEachEdge((_key, _attrs, source, target) => {
		if (nodeIdSet.has(source) && nodeIdSet.has(target) && source !== target) {
			children.get(source)?.push(target);
			hasIncoming.add(target);
		}
	});

	const roots = [...nodeIdSet].filter((id) => !hasIncoming.has(id));
	if (roots.length === 0) {
		roots.push(nodeIds[0]);
	}

	// BFS
	const levels = new Map<string, number>();
	const queue: string[] = [...roots];
	for (const r of roots) levels.set(r, 0);

	while (queue.length > 0) {
		const current = queue.shift()!;
		const level = levels.get(current)!;
		for (const child of children.get(current) || []) {
			if (!levels.has(child)) {
				levels.set(child, level + 1);
				queue.push(child);
			}
		}
	}

	for (const nodeId of nodeIdSet) {
		if (!levels.has(nodeId)) levels.set(nodeId, 0);
	}

	// Group by level
	const byLevel = new Map<number, string[]>();
	for (const [nodeId, level] of levels) {
		if (!byLevel.has(level)) byLevel.set(level, []);
		byLevel.get(level)!.push(nodeId);
	}

	// Compute positions
	const targets = new Map<string, { x: number; y: number }>();

	for (const [level, nodesAtLevel] of byLevel) {
		const count = nodesAtLevel.length;
		let maxNodeSize = 200;
		for (const nid of nodesAtLevel) {
			if (graph.hasNode(nid)) {
				const attrs = graph.getNodeAttributes(nid);
				maxNodeSize = Math.max(maxNodeSize, attrs.width || 200);
			}
		}

		const totalWidth = (count - 1) * (maxNodeSize + nodeGap);
		const startX = -totalWidth / 2;

		for (let i = 0; i < count; i++) {
			const primary = level * levelGap;
			const secondary = startX + i * (maxNodeSize + nodeGap);

			if (direction === 'top-down') {
				targets.set(nodesAtLevel[i], { x: secondary, y: primary });
			} else {
				targets.set(nodesAtLevel[i], { x: primary, y: secondary });
			}
		}
	}

	return targets;
}

// --- Tests ---

describe('tree layout algorithm', () => {
	it('assigns root at level 0, child at level 1', () => {
		const store = freshStore();
		addNode(store, 'A', 0, 0);
		addNode(store, 'B', 100, 100);
		addEdge(store, 'e1', 'A', 'B');

		const graph = store.get(graphAtom);
		const positions = computeTreeLayout(graph, ['A', 'B']);

		// A is root (no incoming), B is child
		expect(positions.get('A')!.y).toBe(0); // level 0
		expect(positions.get('B')!.y).toBe(200); // level 1 (levelGap=200)
	});

	it('centers siblings horizontally', () => {
		const store = freshStore();
		addNode(store, 'root', 0, 0);
		addNode(store, 'left', 0, 0);
		addNode(store, 'right', 0, 0);
		addEdge(store, 'e1', 'root', 'left');
		addEdge(store, 'e2', 'root', 'right');

		const graph = store.get(graphAtom);
		const positions = computeTreeLayout(graph, ['root', 'left', 'right']);

		// Root should be centered (only node at level 0)
		expect(positions.get('root')!.x).toBe(0);

		// Two children at level 1 should be symmetric around center
		const leftX = positions.get('left')!.x;
		const rightX = positions.get('right')!.x;
		expect(leftX).toBeLessThan(rightX);
		expect(leftX + rightX).toBe(0); // symmetric
	});

	it('handles disconnected nodes at level 0', () => {
		const store = freshStore();
		addNode(store, 'A', 0, 0);
		addNode(store, 'B', 100, 0);
		// No edges — both disconnected

		const graph = store.get(graphAtom);
		const positions = computeTreeLayout(graph, ['A', 'B']);

		// Both at level 0
		expect(positions.get('A')!.y).toBe(0);
		expect(positions.get('B')!.y).toBe(0);
	});

	it('supports left-right direction', () => {
		const store = freshStore();
		addNode(store, 'A', 0, 0);
		addNode(store, 'B', 100, 100);
		addEdge(store, 'e1', 'A', 'B');

		const graph = store.get(graphAtom);
		const positions = computeTreeLayout(graph, ['A', 'B'], { direction: 'left-right' });

		// In left-right mode, level maps to x, siblings spread on y
		expect(positions.get('A')!.x).toBe(0); // level 0
		expect(positions.get('B')!.x).toBe(200); // level 1
	});

	it('handles a chain A→B→C correctly', () => {
		const store = freshStore();
		addNode(store, 'A');
		addNode(store, 'B');
		addNode(store, 'C');
		addEdge(store, 'e1', 'A', 'B');
		addEdge(store, 'e2', 'B', 'C');

		const graph = store.get(graphAtom);
		const positions = computeTreeLayout(graph, ['A', 'B', 'C']);

		expect(positions.get('A')!.y).toBe(0);
		expect(positions.get('B')!.y).toBe(200);
		expect(positions.get('C')!.y).toBe(400);
		// All centered at x=0 (single node per level)
		expect(positions.get('A')!.x).toBe(0);
		expect(positions.get('B')!.x).toBe(0);
		expect(positions.get('C')!.x).toBe(0);
	});

	it('handles cycle by picking first node as root', () => {
		const store = freshStore();
		addNode(store, 'A');
		addNode(store, 'B');
		addEdge(store, 'e1', 'A', 'B');
		addEdge(store, 'e2', 'B', 'A');

		const graph = store.get(graphAtom);
		// Both have incoming edges, so roots array will be empty → picks first
		const positions = computeTreeLayout(graph, ['A', 'B']);

		// Should produce valid positions without crashing
		expect(positions.size).toBe(2);
		expect(positions.has('A')).toBe(true);
		expect(positions.has('B')).toBe(true);
	});
});
