import { describe, it, expect } from 'vitest';
import { layoutForest, type TidyNode } from './tidyTree';

// tidyTree is domain-agnostic: it lays out abstract { id, width, children }
// nodes, so these tests use hand-built trees with no PersonNodeDTO import.
const W = 160;
const GAP = 40;

function leaf(id: string, width = W): TidyNode {
	return { id, width, children: [] };
}

function node(id: string, children: TidyNode[], width = W): TidyNode {
	return { id, width, children };
}

function center(x: Map<string, number>, n: TidyNode): number {
	return x.get(n.id)! + n.width / 2;
}

// Walk a forest, recording each node's tree depth and width.
function depths(roots: TidyNode[]): Map<string, { depth: number; width: number }> {
	const out = new Map<string, { depth: number; width: number }>();
	const walk = (n: TidyNode, d: number) => {
		out.set(n.id, { depth: d, width: n.width });
		for (const c of n.children) walk(c, d + 1);
	};
	for (const r of roots) walk(r, 0);
	return out;
}

// Assert no two boxes at the same depth overlap (clearance >= gap).
function expectNoOverlap(x: Map<string, number>, roots: TidyNode[], gap: number) {
	const meta = depths(roots);
	const ids = [...x.keys()];
	for (let i = 0; i < ids.length; i++) {
		for (let j = i + 1; j < ids.length; j++) {
			const a = meta.get(ids[i])!;
			const b = meta.get(ids[j])!;
			if (a.depth !== b.depth) continue;
			const xa = x.get(ids[i])!;
			const xb = x.get(ids[j])!;
			const lo = Math.min(xa, xb);
			const hi = Math.max(xa, xb);
			const loW = xa <= xb ? a.width : b.width;
			expect(hi - lo).toBeGreaterThanOrEqual(loW + gap);
		}
	}
}

describe('tidyTree — leaf base case', () => {
	it('a single leaf lays out at x = 0', () => {
		const a = leaf('a');
		const x = layoutForest([a], GAP);
		expect(x.get('a')).toBe(0);
	});
});

describe('tidyTree — ancestor centring', () => {
	it('a parent is centred over the span of its two children', () => {
		const c1 = leaf('c1');
		const c2 = leaf('c2');
		const p = node('p', [c1, c2]);
		const x = layoutForest([p], GAP);

		const pc = center(x, p);
		const lo = center(x, c1);
		const hi = center(x, c2);
		// Parent sits exactly at the midpoint of its children's centres …
		expect(pc).toBe((lo + hi) / 2);
		// … which is within their span (the named-bug guard generalises this).
		expect(pc).toBeGreaterThanOrEqual(Math.min(lo, hi));
		expect(pc).toBeLessThanOrEqual(Math.max(lo, hi));
		// Children do not overlap.
		expect(Math.abs(x.get('c2')! - x.get('c1')!)).toBeGreaterThanOrEqual(W + GAP);
	});
});

describe('tidyTree — contour nesting', () => {
	it('a deep subtree and a shallow sibling nest without overlap', () => {
		// root
		//  ├─ a (leaf)
		//  └─ b ─ b1, b2  (deeper)
		// The contour push must keep b's whole subtree clear of leaf a, and a
		// clear of b's grandchildren, at every depth.
		const a = leaf('a');
		const b = node('b', [leaf('b1'), leaf('b2')]);
		const root = node('root', [a, b]);
		const x = layoutForest([root], GAP);

		expectNoOverlap(x, [root], GAP);
		// Each parent still centred over its own children.
		expect(center(x, b)).toBe(
			(center(x, { id: 'b1', width: W, children: [] }) +
				center(x, { id: 'b2', width: W, children: [] })) /
				2
		);
	});
});

describe('tidyTree — multi-root packing', () => {
	it('packs multiple roots left-to-right with no overlap', () => {
		// A real Stammbaum is a forest of ~50 roots, not a single tree — this is
		// the dominant compactness win. Three roots of differing shapes.
		const r1 = node('r1', [leaf('r1a'), leaf('r1b')]);
		const r2 = leaf('r2');
		const r3 = node('r3', [leaf('r3a')]);
		const roots = [r1, r2, r3];
		const x = layoutForest(roots, GAP);

		expectNoOverlap(x, roots, GAP);
		// Roots keep input order left-to-right.
		expect(x.get('r1')!).toBeLessThan(x.get('r2')!);
		expect(x.get('r2')!).toBeLessThan(x.get('r3')!);
		// Forest is normalised so the leftmost edge is at 0.
		expect(Math.min(...x.values())).toBe(0);
	});
});

describe('tidyTree — variable-width run', () => {
	it('centres a wide couple run over its children and clears its siblings', () => {
		// A couple unit is one node whose width spans two cards + the gap. It
		// must still centre over its own children and stay clear of siblings by
		// the FULL run width, not a single card.
		const COUPLE = 2 * W + GAP;
		const couple = node('couple', [leaf('g1'), leaf('g2')], COUPLE);
		const sibling = node('sib', [leaf('s1')]);
		const roots = [sibling, couple];
		const x = layoutForest(roots, GAP);

		expectNoOverlap(x, roots, GAP);
		// Couple centred over its two children.
		expect(center(x, couple)).toBe((center(x, leaf('g1')) + center(x, leaf('g2'))) / 2);
		// The sibling root clears the couple's full run width.
		expect(x.get('couple')! - x.get('sib')!).toBeGreaterThanOrEqual(W + GAP);
	});
});
