import { describe, it, expect } from 'vitest';
import { createStore } from 'jotai';
import Graph from 'graphology';
import type { GraphNodeAttributes, GraphEdgeAttributes, DBGraphNode } from '../core/types';
import { graphAtom, graphOptions, currentGraphIdAtom } from '../core/graph-store';
import { addNodeToLocalGraphAtom } from '../core/graph-mutations';
import { selectedNodeIdsAtom } from '../core/selection-store';
import {
	selectionPathAtom,
	isSelectingAtom,
	startSelectionAtom,
	updateSelectionAtom,
	endSelectionAtom,
	selectionRectAtom,
	pointInPolygon,
} from '../core/selection-path-store';

// --- 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, w = 200, h = 100): DBGraphNode {
	return {
		id,
		graph_id: 'test-graph',
		label: `Node ${id}`,
		node_type: 'default',
		configuration: null,
		ui_properties: { x, y, width: w, height: h, size: 15, zIndex: 0 },
		data: 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));
}

// --- Tests ---

describe('selection-path-store', () => {
	describe('pointInPolygon', () => {
		it('returns true for point inside triangle', () => {
			const triangle = [
				{ x: 0, y: 0 },
				{ x: 100, y: 0 },
				{ x: 50, y: 100 },
			];
			expect(pointInPolygon(50, 30, triangle)).toBe(true);
		});

		it('returns false for point outside triangle', () => {
			const triangle = [
				{ x: 0, y: 0 },
				{ x: 100, y: 0 },
				{ x: 50, y: 100 },
			];
			expect(pointInPolygon(200, 200, triangle)).toBe(false);
		});

		it('returns true for point inside square', () => {
			const square = [
				{ x: 0, y: 0 },
				{ x: 100, y: 0 },
				{ x: 100, y: 100 },
				{ x: 0, y: 100 },
			];
			expect(pointInPolygon(50, 50, square)).toBe(true);
		});

		it('returns false for point outside square', () => {
			const square = [
				{ x: 0, y: 0 },
				{ x: 100, y: 0 },
				{ x: 100, y: 100 },
				{ x: 0, y: 100 },
			];
			expect(pointInPolygon(-10, 50, square)).toBe(false);
		});

		it('handles concave polygon', () => {
			// L-shaped polygon
			const lShape = [
				{ x: 0, y: 0 },
				{ x: 50, y: 0 },
				{ x: 50, y: 50 },
				{ x: 100, y: 50 },
				{ x: 100, y: 100 },
				{ x: 0, y: 100 },
			];
			expect(pointInPolygon(25, 25, lShape)).toBe(true);   // inside the L
			expect(pointInPolygon(75, 25, lShape)).toBe(false);  // in the notch
			expect(pointInPolygon(75, 75, lShape)).toBe(true);   // inside the L bottom
		});
	});

	describe('selection path lifecycle', () => {
		it('starts with no selection path', () => {
			const store = freshStore();
			expect(store.get(selectionPathAtom)).toBeNull();
			expect(store.get(isSelectingAtom)).toBe(false);
		});

		it('startSelection creates a path', () => {
			const store = freshStore();
			store.set(startSelectionAtom, { type: 'rect', point: { x: 10, y: 20 } });
			const path = store.get(selectionPathAtom);
			expect(path).not.toBeNull();
			expect(path!.type).toBe('rect');
			expect(path!.points).toHaveLength(1);
			expect(store.get(isSelectingAtom)).toBe(true);
		});

		it('updateSelection adds point for lasso', () => {
			const store = freshStore();
			store.set(startSelectionAtom, { type: 'lasso', point: { x: 0, y: 0 } });
			store.set(updateSelectionAtom, { x: 50, y: 0 });
			store.set(updateSelectionAtom, { x: 50, y: 50 });
			const path = store.get(selectionPathAtom);
			expect(path!.points).toHaveLength(3);
		});

		it('updateSelection keeps only 2 points for rect', () => {
			const store = freshStore();
			store.set(startSelectionAtom, { type: 'rect', point: { x: 0, y: 0 } });
			store.set(updateSelectionAtom, { x: 50, y: 50 });
			store.set(updateSelectionAtom, { x: 100, y: 100 });
			const path = store.get(selectionPathAtom);
			expect(path!.points).toHaveLength(2);
			expect(path!.points[0]).toEqual({ x: 0, y: 0 });
			expect(path!.points[1]).toEqual({ x: 100, y: 100 });
		});

		it('endSelection clears the path', () => {
			const store = freshStore();
			store.set(startSelectionAtom, { type: 'rect', point: { x: 0, y: 0 } });
			store.set(updateSelectionAtom, { x: 100, y: 100 });
			store.set(endSelectionAtom);
			expect(store.get(selectionPathAtom)).toBeNull();
			expect(store.get(isSelectingAtom)).toBe(false);
		});

		it('endSelection with < 2 points just clears', () => {
			const store = freshStore();
			store.set(startSelectionAtom, { type: 'rect', point: { x: 0, y: 0 } });
			// No update — only 1 point
			store.set(endSelectionAtom);
			expect(store.get(selectionPathAtom)).toBeNull();
			expect(store.get(selectedNodeIdsAtom).size).toBe(0);
		});

		it('updateSelection on null path is a no-op', () => {
			const store = freshStore();
			store.set(updateSelectionAtom, { x: 10, y: 20 });
			expect(store.get(selectionPathAtom)).toBeNull();
		});
	});

	describe('selectionRectAtom', () => {
		it('returns null when no path', () => {
			const store = freshStore();
			expect(store.get(selectionRectAtom)).toBeNull();
		});

		it('returns null for lasso path', () => {
			const store = freshStore();
			store.set(startSelectionAtom, { type: 'lasso', point: { x: 0, y: 0 } });
			store.set(updateSelectionAtom, { x: 50, y: 50 });
			expect(store.get(selectionRectAtom)).toBeNull();
		});

		it('computes correct rect bounds', () => {
			const store = freshStore();
			store.set(startSelectionAtom, { type: 'rect', point: { x: 100, y: 200 } });
			store.set(updateSelectionAtom, { x: 50, y: 300 });
			const rect = store.get(selectionRectAtom);
			expect(rect).toEqual({ x: 50, y: 200, width: 50, height: 100 });
		});

		it('handles negative direction drag', () => {
			const store = freshStore();
			store.set(startSelectionAtom, { type: 'rect', point: { x: 200, y: 200 } });
			store.set(updateSelectionAtom, { x: 50, y: 50 });
			const rect = store.get(selectionRectAtom);
			expect(rect).toEqual({ x: 50, y: 50, width: 150, height: 150 });
		});
	});

	describe('rect selection — node intersection', () => {
		it('selects nodes inside the rect', () => {
			const store = freshStore();
			addNode(store, 'n1', 50, 50);     // inside 0,0 → 300,300
			addNode(store, 'n2', 500, 500);   // outside
			addNode(store, 'n3', 100, 100);   // inside

			store.set(startSelectionAtom, { type: 'rect', point: { x: 0, y: 0 } });
			store.set(updateSelectionAtom, { x: 300, y: 300 });
			store.set(endSelectionAtom);

			const selected = store.get(selectedNodeIdsAtom);
			expect(selected.has('n1')).toBe(true);
			expect(selected.has('n3')).toBe(true);
			expect(selected.has('n2')).toBe(false);
		});

		it('selects nodes that partially overlap the rect', () => {
			const store = freshStore();
			// Node at (150, 50) with default size 200x100 → extends to (350, 150)
			// Rect from (0,0) to (160,60) → overlaps node
			addNode(store, 'n1', 150, 50);

			store.set(startSelectionAtom, { type: 'rect', point: { x: 0, y: 0 } });
			store.set(updateSelectionAtom, { x: 160, y: 60 });
			store.set(endSelectionAtom);

			const selected = store.get(selectedNodeIdsAtom);
			expect(selected.has('n1')).toBe(true);
		});

		it('does not select nodes outside the rect', () => {
			const store = freshStore();
			addNode(store, 'n1', 500, 500);

			store.set(startSelectionAtom, { type: 'rect', point: { x: 0, y: 0 } });
			store.set(updateSelectionAtom, { x: 100, y: 100 });
			store.set(endSelectionAtom);

			expect(store.get(selectedNodeIdsAtom).size).toBe(0);
		});
	});

	describe('lasso selection — node intersection', () => {
		it('selects nodes whose center is inside the lasso', () => {
			const store = freshStore();
			// Node at (50, 50) → center is (150, 100)
			addNode(store, 'n1', 50, 50);
			// Node at (500, 500) → center is (600, 550)
			addNode(store, 'n2', 500, 500);

			// Draw a lasso around n1's center
			store.set(startSelectionAtom, { type: 'lasso', point: { x: 0, y: 0 } });
			store.set(updateSelectionAtom, { x: 300, y: 0 });
			store.set(updateSelectionAtom, { x: 300, y: 200 });
			store.set(updateSelectionAtom, { x: 0, y: 200 });
			store.set(endSelectionAtom);

			const selected = store.get(selectedNodeIdsAtom);
			expect(selected.has('n1')).toBe(true);
			expect(selected.has('n2')).toBe(false);
		});

		it('handles empty lasso (no nodes selected)', () => {
			const store = freshStore();
			addNode(store, 'n1', 500, 500);

			store.set(startSelectionAtom, { type: 'lasso', point: { x: 0, y: 0 } });
			store.set(updateSelectionAtom, { x: 10, y: 0 });
			store.set(updateSelectionAtom, { x: 10, y: 10 });
			store.set(endSelectionAtom);

			expect(store.get(selectedNodeIdsAtom).size).toBe(0);
		});
	});
});
