import { describe, it, expect } from 'vitest';
import { createStore } from 'jotai';
import type { DBGraphNode, DBGraphEdge } from '../core/types';
import {
	VIRTUALIZATION_BUFFER,
	virtualizationEnabledAtom,
	visibleBoundsAtom,
	visibleNodeKeysAtom,
	visibleEdgeKeysAtom,
	virtualizationMetricsAtom,
} from '../core/virtualization-store';
import Graph from 'graphology';
import type { GraphNodeAttributes, GraphEdgeAttributes } from '../core/types';
import { graphAtom, graphOptions, currentGraphIdAtom } from '../core/graph-store';
import { nodeKeysAtom } from '../core/graph-derived';
import { addNodeToLocalGraphAtom } from '../core/graph-mutations';
import { addEdgeToLocalGraphAtom } from '../core/graph-mutations-edges';
import { panAtom, zoomAtom, viewportRectAtom } from '../core/viewport-store';
import { setVirtualizationEnabledAtom } from '../core/settings-store';

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: number, y: number, width = 200, height = 100): DBGraphNode {
	return {
		id,
		graph_id: 'test-graph',
		label: `Node ${id}`,
		node_type: 'default',
		configuration: null,
		ui_properties: { x, y, width, height, 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: 'default',
		filter_condition: null,
		ui_properties: null,
		data: null,
		created_at: '2024-01-01T00:00:00Z',
		updated_at: '2024-01-01T00:00:00Z',
	};
}

function makeDOMRect(x: number, y: number, width: number, height: number): DOMRect {
	return {
		x, y, width, height,
		top: y,
		left: x,
		right: x + width,
		bottom: y + height,
		toJSON: () => ({}),
	};
}

describe('virtualization-store', () => {
	describe('configuration', () => {
		it('has correct buffer value', () => {
			expect(VIRTUALIZATION_BUFFER).toBe(200);
		});
	});

	// --- Visible Bounds ---

	describe('visibleBoundsAtom', () => {
		it('returns null without viewport', () => {
			const store = freshStore();
			expect(store.get(visibleBoundsAtom)).toBeNull();
		});

		it('computes visible bounds from viewport, pan, zoom', () => {
			const store = freshStore();
			store.set(viewportRectAtom, makeDOMRect(0, 0, 1000, 800));
			store.set(panAtom, { x: 0, y: 0 });
			store.set(zoomAtom, 1);

			const bounds = store.get(visibleBoundsAtom)!;
			expect(bounds).not.toBeNull();
			expect(bounds.minX).toBe(-VIRTUALIZATION_BUFFER);
			expect(bounds.minY).toBe(-VIRTUALIZATION_BUFFER);
			expect(bounds.maxX).toBe(1000 + VIRTUALIZATION_BUFFER);
			expect(bounds.maxY).toBe(800 + VIRTUALIZATION_BUFFER);
		});

		it('accounts for zoom', () => {
			const store = freshStore();
			store.set(viewportRectAtom, makeDOMRect(0, 0, 1000, 800));
			store.set(panAtom, { x: 0, y: 0 });
			store.set(zoomAtom, 2);

			const bounds = store.get(visibleBoundsAtom)!;
			// At 2x zoom, world coords are halved
			expect(bounds.maxX).toBe((1000 + VIRTUALIZATION_BUFFER) / 2);
			expect(bounds.maxY).toBe((800 + VIRTUALIZATION_BUFFER) / 2);
		});

		it('accounts for pan', () => {
			const store = freshStore();
			store.set(viewportRectAtom, makeDOMRect(0, 0, 1000, 800));
			store.set(panAtom, { x: -500, y: -300 });
			store.set(zoomAtom, 1);

			const bounds = store.get(visibleBoundsAtom)!;
			// Pan of -500 shifts visible bounds to the right in world space
			expect(bounds.minX).toBe(-VIRTUALIZATION_BUFFER + 500);
			expect(bounds.minY).toBe(-VIRTUALIZATION_BUFFER + 300);
		});
	});

	// --- Node Visibility ---

	describe('visibleNodeKeysAtom', () => {
		it('returns all nodes when virtualization disabled', () => {
			const store = freshStore();
			store.set(setVirtualizationEnabledAtom, false);
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', 0, 0));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n2', 99999, 99999));

			const visible = store.get(visibleNodeKeysAtom);
			expect(visible).toHaveLength(2);
		});

		it('returns all nodes when no viewport', () => {
			const store = freshStore();
			store.set(setVirtualizationEnabledAtom, true);
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', 0, 0));

			const visible = store.get(visibleNodeKeysAtom);
			expect(visible).toHaveLength(1);
		});

		it('culls nodes outside viewport', () => {
			const store = freshStore();
			store.set(setVirtualizationEnabledAtom, true);
			store.set(viewportRectAtom, makeDOMRect(0, 0, 1000, 800));
			store.set(panAtom, { x: 0, y: 0 });
			store.set(zoomAtom, 1);

			// Node inside viewport
			store.set(addNodeToLocalGraphAtom, makeDbNode('visible', 100, 100));
			// Node far outside viewport (beyond buffer)
			store.set(addNodeToLocalGraphAtom, makeDbNode('hidden', 5000, 5000));

			const visible = store.get(visibleNodeKeysAtom);
			expect(visible).toContain('visible');
			expect(visible).not.toContain('hidden');
		});

		it('includes nodes within buffer zone', () => {
			const store = freshStore();
			store.set(setVirtualizationEnabledAtom, true);
			store.set(viewportRectAtom, makeDOMRect(0, 0, 1000, 800));
			store.set(panAtom, { x: 0, y: 0 });
			store.set(zoomAtom, 1);

			// Node just outside viewport but within buffer (200px)
			store.set(addNodeToLocalGraphAtom, makeDbNode('buffer-node', 1050, 100, 200, 100));

			const visible = store.get(visibleNodeKeysAtom);
			expect(visible).toContain('buffer-node');
		});
	});

	// --- Edge Visibility ---

	describe('visibleEdgeKeysAtom', () => {
		it('returns all edges when virtualization disabled', () => {
			const store = freshStore();
			store.set(setVirtualizationEnabledAtom, false);
			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', 0, 0));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n2', 99999, 99999));
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));

			const visible = store.get(visibleEdgeKeysAtom);
			expect(visible).toContain('e1');
		});

		it('hides edges where both endpoints are hidden', () => {
			const store = freshStore();
			store.set(setVirtualizationEnabledAtom, true);
			store.set(viewportRectAtom, makeDOMRect(0, 0, 1000, 800));
			store.set(panAtom, { x: 0, y: 0 });
			store.set(zoomAtom, 1);

			store.set(addNodeToLocalGraphAtom, makeDbNode('visible', 100, 100));
			store.set(addNodeToLocalGraphAtom, makeDbNode('hidden1', 5000, 5000));
			store.set(addNodeToLocalGraphAtom, makeDbNode('hidden2', 6000, 6000));
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e-hidden', 'hidden1', 'hidden2'));
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e-partial', 'visible', 'hidden1'));

			const visible = store.get(visibleEdgeKeysAtom);
			expect(visible).not.toContain('e-hidden');
			// Edge with one visible endpoint is also hidden (both must be visible)
			expect(visible).not.toContain('e-partial');
		});

		it('shows edges where both endpoints are visible', () => {
			const store = freshStore();
			store.set(setVirtualizationEnabledAtom, true);
			store.set(viewportRectAtom, makeDOMRect(0, 0, 1000, 800));
			store.set(panAtom, { x: 0, y: 0 });
			store.set(zoomAtom, 1);

			store.set(addNodeToLocalGraphAtom, makeDbNode('n1', 100, 100));
			store.set(addNodeToLocalGraphAtom, makeDbNode('n2', 300, 300));
			store.set(addEdgeToLocalGraphAtom, makeDbEdge('e1', 'n1', 'n2'));

			const visible = store.get(visibleEdgeKeysAtom);
			expect(visible).toContain('e1');
		});
	});

	// --- Metrics ---

	describe('virtualizationMetricsAtom', () => {
		it('returns correct metrics', () => {
			const store = freshStore();
			store.set(setVirtualizationEnabledAtom, true);
			store.set(viewportRectAtom, makeDOMRect(0, 0, 1000, 800));
			store.set(panAtom, { x: 0, y: 0 });
			store.set(zoomAtom, 1);

			store.set(addNodeToLocalGraphAtom, makeDbNode('visible', 100, 100));
			store.set(addNodeToLocalGraphAtom, makeDbNode('hidden', 5000, 5000));

			const metrics = store.get(virtualizationMetricsAtom);
			expect(metrics.enabled).toBe(true);
			expect(metrics.totalNodes).toBe(2);
			expect(metrics.visibleNodes).toBe(1);
			expect(metrics.culledNodes).toBe(1);
		});
	});

	// --- Toggle ---

	describe('toggle virtualization', () => {
		it('can be toggled on/off', () => {
			const store = freshStore();
			store.set(setVirtualizationEnabledAtom, true);
			expect(store.get(virtualizationEnabledAtom)).toBe(true);

			store.set(setVirtualizationEnabledAtom, false);
			expect(store.get(virtualizationEnabledAtom)).toBe(false);
		});
	});
});
