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

// --- Helpers ---

function makeDbNode(id: string, x = 0, y = 0, w = 300, h = 200): DBGraphNode {
	return {
		id,
		graph_id: 'test',
		label: `Node ${id}`,
		node_type: 'default' as const,
		configuration: null,
		data: null,
		ui_properties: { x, y, width: w, height: h, size: 10, zIndex: 0 },
		created_at: '2024-01-01T00:00:00Z',
		updated_at: '2024-01-01T00:00:00Z',
	};
}

function freshStore() {
	const store = createStore();
	store.set(graphAtom, new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions));
	store.set(currentGraphIdAtom, 'test');
	return store;
}

/**
 * Pure resize math: compute new dimensions given a resize direction and delta.
 * Mirrors the logic in useNodeResize.
 */
function computeResize(
	direction: string,
	startW: number,
	startH: number,
	startX: number,
	startY: number,
	deltaX: number,
	deltaY: number,
	minW = 200,
	minH = 150,
) {
	let newWidth = startW;
	let newHeight = startH;
	let newX = startX;
	let newY = startY;

	if (direction.includes('e')) {
		newWidth = Math.max(minW, startW + deltaX);
	}
	if (direction.includes('w')) {
		newWidth = Math.max(minW, startW - deltaX);
		newX = startX + (startW - newWidth);
	}
	if (direction.includes('s')) {
		newHeight = Math.max(minH, startH + deltaY);
	}
	if (direction.includes('n')) {
		newHeight = Math.max(minH, startH - deltaY);
		newY = startY + (startH - newHeight);
	}

	return { width: newWidth, height: newHeight, x: newX, y: newY };
}

// --- Tests ---

describe('useNodeResize atoms — resize direction math', () => {
	describe('east resize', () => {
		it('width increases by deltaX, x unchanged', () => {
			const result = computeResize('e', 300, 200, 100, 100, 50, 0);
			expect(result.width).toBe(350);
			expect(result.x).toBe(100);
			expect(result.height).toBe(200);
			expect(result.y).toBe(100);
		});
	});

	describe('west resize', () => {
		it('width increases by abs(deltaX), x shifts left', () => {
			const result = computeResize('w', 300, 200, 100, 100, -50, 0);
			expect(result.width).toBe(350);
			expect(result.x).toBe(50);
			expect(result.height).toBe(200);
			expect(result.y).toBe(100);
		});
	});

	describe('south resize', () => {
		it('height increases by deltaY, y unchanged', () => {
			const result = computeResize('s', 300, 200, 100, 100, 0, 60);
			expect(result.height).toBe(260);
			expect(result.y).toBe(100);
			expect(result.width).toBe(300);
			expect(result.x).toBe(100);
		});
	});

	describe('north resize', () => {
		it('height increases by abs(deltaY), y shifts up', () => {
			const result = computeResize('n', 300, 200, 100, 100, 0, -40);
			expect(result.height).toBe(240);
			expect(result.y).toBe(60);
			expect(result.width).toBe(300);
			expect(result.x).toBe(100);
		});
	});

	describe('southeast resize', () => {
		it('both width and height increase', () => {
			const result = computeResize('se', 300, 200, 100, 100, 50, 60);
			expect(result.width).toBe(350);
			expect(result.height).toBe(260);
			expect(result.x).toBe(100);
			expect(result.y).toBe(100);
		});
	});

	describe('northwest resize', () => {
		it('both dimensions increase, both x/y shift', () => {
			const result = computeResize('nw', 300, 200, 100, 100, -50, -40);
			expect(result.width).toBe(350);
			expect(result.height).toBe(240);
			expect(result.x).toBe(50);
			expect(result.y).toBe(60);
		});
	});

	describe('min width enforcement', () => {
		it('width cannot go below minWidth (200)', () => {
			// Shrinking east by more than width allows
			const result = computeResize('e', 250, 200, 100, 100, -100, 0);
			expect(result.width).toBe(200);
		});

		it('west resize enforces min width', () => {
			const result = computeResize('w', 250, 200, 100, 100, 100, 0);
			expect(result.width).toBe(200);
			// x should shift to compensate
			expect(result.x).toBe(150);
		});
	});

	describe('min height enforcement', () => {
		it('height cannot go below minHeight (150)', () => {
			const result = computeResize('s', 300, 200, 100, 100, 0, -100);
			expect(result.height).toBe(150);
		});

		it('north resize enforces min height', () => {
			const result = computeResize('n', 300, 200, 100, 100, 0, 100);
			expect(result.height).toBe(150);
			// y should shift to compensate
			expect(result.y).toBe(150);
		});
	});

	describe('graphUpdateVersionAtom during resize', () => {
		it('increments when graph attributes change', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('a', 100, 100, 300, 200));

			const versionBefore = store.get(graphUpdateVersionAtom);

			// Simulate resize by directly updating graph attributes
			const graph = store.get(graphAtom);
			graph.setNodeAttribute('a', 'width', 400);
			graph.setNodeAttribute('a', 'height', 300);
			store.set(graphUpdateVersionAtom, (v) => v + 1);

			expect(store.get(graphUpdateVersionAtom)).toBe(versionBefore + 1);
		});
	});

	describe('nodePositionUpdateCounterAtom during resize', () => {
		it('increments for north/west directions that change position', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('a', 100, 100, 300, 200));

			const counterBefore = store.get(nodePositionUpdateCounterAtom);

			// North/west resize changes node position
			const resized = computeResize('nw', 300, 200, 100, 100, -50, -40);
			store.set(updateNodePositionAtom, {
				nodeId: 'a',
				position: { x: resized.x, y: resized.y },
			});

			expect(store.get(nodePositionUpdateCounterAtom)).toBe(counterBefore + 1);
		});

		it('does not increment for east/south directions (position unchanged)', () => {
			const store = freshStore();
			store.set(addNodeToLocalGraphAtom, makeDbNode('a', 100, 100, 300, 200));

			const counterBefore = store.get(nodePositionUpdateCounterAtom);

			// East/south resize does not change node x/y position,
			// so no updateNodePositionAtom call is needed
			const resized = computeResize('se', 300, 200, 100, 100, 50, 60);
			// Only width/height change, position stays at 100,100
			expect(resized.x).toBe(100);
			expect(resized.y).toBe(100);

			// Counter should remain unchanged since no position update was needed
			expect(store.get(nodePositionUpdateCounterAtom)).toBe(counterBefore);
		});
	});
});
