// Sanity floors for the canonical Stammbaum fixture. Owned by the fixture
// directory rather than the capture script so the contract lives next to the
// JSON it constrains and is reachable from unit tests.
//
// Calibrated against the May-2026 canonical dataset (62 nodes, 5 generations,
// 28 SPOUSE_OF edges, 1 multi-spouse person). The point of these gates is
// catching a silently empty backend or a structurally regressed snapshot, not
// strict size validation; raise them only if the canonical graph grows
// substantially.

export const MIN_NODES = 50;
export const MIN_GENERATIONS = 5;
export const MIN_SPOUSE_OF_EDGES = 1;
export const MIN_MULTI_SPOUSE_PERSONS = 1;

type Edge = { relationType?: string; personId?: string; relatedPersonId?: string };
type Node = { id?: string; generation?: number | null };
export type NetworkShape = { nodes?: Node[]; edges?: Edge[] };

// The AC3 revisit-trigger predicate lives in a plain .mjs module so the
// Node-run capture script and this TypeScript validator share one
// implementation. Re-exported here so consumers of the fixture-directory
// barrel keep their existing import surface.
export { findAc3Candidates } from './findAc3Candidates.mjs';

export type FixtureStats = {
	nodes: number;
	edges: number;
	spouseEdges: number;
	generations: number[];
	multiSpousePersons: number;
};

export function validateFixture(network: NetworkShape): FixtureStats {
	const nodes = Array.isArray(network.nodes) ? network.nodes : [];
	const edges = Array.isArray(network.edges) ? network.edges : [];
	const spouseEdges = edges.filter((e) => e.relationType === 'SPOUSE_OF');
	const generations = new Set(nodes.map((n) => n.generation).filter((g) => g != null));
	const multiSpousePersons = countMultiSpousePersons(spouseEdges);

	const failures: string[] = [];
	if (nodes.length < MIN_NODES) {
		failures.push(`expected >= ${MIN_NODES} nodes, got ${nodes.length}`);
	}
	if (generations.size < MIN_GENERATIONS) {
		failures.push(`expected >= ${MIN_GENERATIONS} distinct generations, got ${generations.size}`);
	}
	if (spouseEdges.length < MIN_SPOUSE_OF_EDGES) {
		failures.push(`expected >= ${MIN_SPOUSE_OF_EDGES} SPOUSE_OF edges, got ${spouseEdges.length}`);
	}
	if (multiSpousePersons < MIN_MULTI_SPOUSE_PERSONS) {
		// buildLayout.test.ts asserts the multi-spouse property on this
		// fixture; a recapture that loses every multi-spouse person would
		// silently make that test vacuous. Fail loudly instead.
		failures.push(
			`expected >= ${MIN_MULTI_SPOUSE_PERSONS} person with multiple SPOUSE_OF edges, ` +
				`got ${multiSpousePersons}`
		);
	}
	if (failures.length > 0) {
		throw new Error(`Sanity gates failed:\n  - ${failures.join('\n  - ')}`);
	}

	return {
		nodes: nodes.length,
		edges: edges.length,
		spouseEdges: spouseEdges.length,
		generations: [...generations].sort((a, b) => (a as number) - (b as number)) as number[],
		multiSpousePersons
	};
}

function countMultiSpousePersons(spouseEdges: Edge[]): number {
	const partners = new Map<string, Set<string>>();
	for (const e of spouseEdges) {
		if (!e.personId || !e.relatedPersonId) continue;
		addPartner(partners, e.personId, e.relatedPersonId);
		addPartner(partners, e.relatedPersonId, e.personId);
	}
	let count = 0;
	for (const set of partners.values()) {
		if (set.size >= 2) count += 1;
	}
	return count;
}

function addPartner(map: Map<string, Set<string>>, key: string, value: string) {
	const s = map.get(key);
	if (s) s.add(value);
	else map.set(key, new Set([value]));
}
