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 {
	zoomAtom,
	panAtom,
	viewportRectAtom,
	zoomFocusNodeIdAtom,
	zoomTransitionProgressAtom,
	isZoomTransitioningAtom,
	zoomAnimationTargetAtom,
	animateZoomToNodeAtom,
	animateFitToBoundsAtom,
} from '../core/viewport-store';

// --- Helpers ---

function freshStore() {
	const store = createStore();
	store.set(graphAtom, new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions));
	store.set(currentGraphIdAtom, 'test-graph');
	// Set a viewport rect for tests
	store.set(viewportRectAtom, { x: 0, y: 0, width: 800, height: 600, top: 0, left: 0, right: 800, bottom: 600, toJSON: () => ({}) } as DOMRect);
	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('zoom transition atoms', () => {
	describe('initial state', () => {
		it('starts with no animation target', () => {
			const store = freshStore();
			expect(store.get(zoomAnimationTargetAtom)).toBeNull();
		});

		it('isZoomTransitioning is false at start', () => {
			const store = freshStore();
			expect(store.get(isZoomTransitioningAtom)).toBe(false);
		});

		it('zoomTransitionProgress is 0 at start', () => {
			const store = freshStore();
			expect(store.get(zoomTransitionProgressAtom)).toBe(0);
		});

		it('zoomFocusNodeId is null at start', () => {
			const store = freshStore();
			expect(store.get(zoomFocusNodeIdAtom)).toBeNull();
		});
	});

	describe('animateZoomToNodeAtom', () => {
		it('sets animation target for existing node', () => {
			const store = freshStore();
			addNode(store, 'n1', 100, 100);

			store.set(animateZoomToNodeAtom, { nodeId: 'n1', duration: 300 });

			const target = store.get(zoomAnimationTargetAtom);
			expect(target).not.toBeNull();
			expect(target!.duration).toBe(300);
			expect(target!.startZoom).toBe(1); // default zoom
			expect(target!.targetZoom).toBe(1); // defaults to current zoom when not specified
		});

		it('sets focus node ID', () => {
			const store = freshStore();
			addNode(store, 'n1', 100, 100);

			store.set(animateZoomToNodeAtom, { nodeId: 'n1' });
			expect(store.get(zoomFocusNodeIdAtom)).toBe('n1');
		});

		it('uses specified targetZoom', () => {
			const store = freshStore();
			addNode(store, 'n1', 100, 100);

			store.set(animateZoomToNodeAtom, { nodeId: 'n1', targetZoom: 2.5 });

			const target = store.get(zoomAnimationTargetAtom);
			expect(target!.targetZoom).toBe(2.5);
		});

		it('captures current zoom/pan as start values', () => {
			const store = freshStore();
			addNode(store, 'n1', 100, 100);
			store.set(zoomAtom, 1.5);
			store.set(panAtom, { x: 50, y: -30 });

			store.set(animateZoomToNodeAtom, { nodeId: 'n1' });

			const target = store.get(zoomAnimationTargetAtom);
			expect(target!.startZoom).toBe(1.5);
			expect(target!.startPan).toEqual({ x: 50, y: -30 });
		});

		it('does nothing for nonexistent node', () => {
			const store = freshStore();
			store.set(animateZoomToNodeAtom, { nodeId: 'nonexistent' });
			expect(store.get(zoomAnimationTargetAtom)).toBeNull();
		});

		it('computes target pan to center the node', () => {
			const store = freshStore();
			// Node at (100, 100) with size 200x100 → center is (200, 150)
			addNode(store, 'n1', 100, 100);

			store.set(animateZoomToNodeAtom, { nodeId: 'n1', targetZoom: 1 });

			const target = store.get(zoomAnimationTargetAtom);
			// With viewport 800x600, center at zoom=1:
			// panX = 400 - 200*1 = 200
			// panY = 300 - 150*1 = 150
			expect(target!.targetPan.x).toBe(200);
			expect(target!.targetPan.y).toBe(150);
		});

		it('defaults duration to 300ms', () => {
			const store = freshStore();
			addNode(store, 'n1', 0, 0);

			store.set(animateZoomToNodeAtom, { nodeId: 'n1' });
			expect(store.get(zoomAnimationTargetAtom)!.duration).toBe(300);
		});
	});

	describe('animateFitToBoundsAtom', () => {
		it('sets animation target for graph mode', () => {
			const store = freshStore();
			addNode(store, 'n1', 0, 0);
			addNode(store, 'n2', 300, 300);

			store.set(animateFitToBoundsAtom, { mode: 'graph' });

			const target = store.get(zoomAnimationTargetAtom);
			expect(target).not.toBeNull();
			expect(target!.targetZoom).toBeGreaterThan(0);
		});

		it('does nothing with no nodes', () => {
			const store = freshStore();
			store.set(animateFitToBoundsAtom, { mode: 'graph' });
			// No nodes → bounds are empty → no animation
			expect(store.get(zoomAnimationTargetAtom)).toBeNull();
		});

		it('captures start state', () => {
			const store = freshStore();
			addNode(store, 'n1', 0, 0);
			store.set(zoomAtom, 2);
			store.set(panAtom, { x: 100, y: 100 });

			store.set(animateFitToBoundsAtom, { mode: 'graph' });

			const target = store.get(zoomAnimationTargetAtom);
			expect(target!.startZoom).toBe(2);
			expect(target!.startPan).toEqual({ x: 100, y: 100 });
		});

		it('uses custom duration', () => {
			const store = freshStore();
			addNode(store, 'n1', 0, 0);

			store.set(animateFitToBoundsAtom, { mode: 'graph', duration: 500 });
			expect(store.get(zoomAnimationTargetAtom)!.duration).toBe(500);
		});
	});

	describe('isZoomTransitioningAtom', () => {
		it('returns true when progress is between 0 and 1', () => {
			const store = freshStore();
			store.set(zoomTransitionProgressAtom, 0.5);
			expect(store.get(isZoomTransitioningAtom)).toBe(true);
		});

		it('returns false when progress is 0', () => {
			const store = freshStore();
			store.set(zoomTransitionProgressAtom, 0);
			expect(store.get(isZoomTransitioningAtom)).toBe(false);
		});

		it('returns false when progress is 1', () => {
			const store = freshStore();
			store.set(zoomTransitionProgressAtom, 1);
			expect(store.get(isZoomTransitioningAtom)).toBe(false);
		});
	});
});
