import { describe, it, expect } from 'vitest';
import { createStore } from 'jotai';
import Graph from 'graphology';
import type { GraphNodeAttributes, GraphEdgeAttributes, DBGraphNode, DBGraphEdge } from '../core/types';
import { graphAtom, graphOptions, currentGraphIdAtom } from '../core/graph-store';
import { zoomAtom, panAtom } from '../core/viewport-store';
import { collapsedGroupsAtom } from '../core/group-store';
import { graphUpdateVersionAtom } from '../core/graph-store';
import { nodePositionUpdateCounterAtom } from '../core/graph-position';
import {
	exportGraph,
	importGraph,
	validateSnapshot,
	SNAPSHOT_VERSION,
	type CanvasSnapshot,
	type SerializedNode,
	type SerializedEdge,
} from '../core/canvas-serializer';

// =============================================================================
// Helpers
// =============================================================================

function makeDbNode(id: string, x = 0, y = 0): DBGraphNode {
	return {
		id,
		graph_id: 'test-graph',
		label: `Node ${id}`,
		node_type: 'default',
		configuration: null,
		ui_properties: { x, y, width: 200, height: 100 },
		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 createPopulatedStore() {
	const store = createStore();
	const graph = new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions);

	graph.addNode('n1', {
		x: 100, y: 200, width: 200, height: 100, size: 15, color: '#fff',
		zIndex: 0, label: 'Node 1', dbData: makeDbNode('n1', 100, 200),
	});
	graph.addNode('n2', {
		x: 300, y: 400, width: 250, height: 120, size: 15, color: '#f00',
		zIndex: 1, label: 'Node 2', dbData: makeDbNode('n2', 300, 400),
	});
	graph.addEdgeWithKey('e1', 'n1', 'n2', {
		weight: 1, type: 'solid', color: '#999',
		dbData: makeDbEdge('e1', 'n1', 'n2'),
	});

	store.set(graphAtom, graph);
	store.set(zoomAtom, 0.75);
	store.set(panAtom, { x: 50, y: -30 });
	return store;
}

function createMinimalSnapshot(overrides: Partial<CanvasSnapshot> = {}): CanvasSnapshot {
	return {
		version: SNAPSHOT_VERSION,
		exportedAt: '2024-01-01T00:00:00Z',
		nodes: [],
		edges: [],
		groups: [],
		viewport: { zoom: 1, pan: { x: 0, y: 0 } },
		...overrides,
	};
}

// =============================================================================
// Tests
// =============================================================================

describe('canvas-serializer', () => {
	describe('exportGraph', () => {
		it('exports nodes with positions and dimensions', () => {
			const store = createPopulatedStore();
			const snapshot = exportGraph(store);

			expect(snapshot.nodes).toHaveLength(2);
			const n1 = snapshot.nodes.find(n => n.id === 'n1')!;
			expect(n1.position).toEqual({ x: 100, y: 200 });
			expect(n1.dimensions).toEqual({ width: 200, height: 100 });
			expect(n1.label).toBe('Node 1');
			expect(n1.dbData.id).toBe('n1');
		});

		it('exports edges with source/target', () => {
			const store = createPopulatedStore();
			const snapshot = exportGraph(store);

			expect(snapshot.edges).toHaveLength(1);
			expect(snapshot.edges[0].sourceId).toBe('n1');
			expect(snapshot.edges[0].targetId).toBe('n2');
			expect(snapshot.edges[0].attributes.type).toBe('solid');
		});

		it('exports viewport state', () => {
			const store = createPopulatedStore();
			const snapshot = exportGraph(store);

			expect(snapshot.viewport.zoom).toBe(0.75);
			expect(snapshot.viewport.pan).toEqual({ x: 50, y: -30 });
		});

		it('sets version and exportedAt', () => {
			const store = createPopulatedStore();
			const snapshot = exportGraph(store);

			expect(snapshot.version).toBe(SNAPSHOT_VERSION);
			expect(typeof snapshot.exportedAt).toBe('string');
			// ISO 8601 format
			expect(() => new Date(snapshot.exportedAt)).not.toThrow();
		});

		it('includes user metadata when provided', () => {
			const store = createPopulatedStore();
			const snapshot = exportGraph(store, { author: 'test', version: '1.0' });

			expect(snapshot.metadata).toEqual({ author: 'test', version: '1.0' });
		});

		it('exports empty graph', () => {
			const store = createStore();
			const snapshot = exportGraph(store);

			expect(snapshot.nodes).toHaveLength(0);
			expect(snapshot.edges).toHaveLength(0);
			expect(snapshot.groups).toHaveLength(0);
		});

		it('exports group relationships', () => {
			const store = createStore();
			const graph = new Graph<GraphNodeAttributes, GraphEdgeAttributes>(graphOptions);
			graph.addNode('group1', {
				x: 0, y: 0, width: 500, height: 400, size: 15, color: '#ccc',
				zIndex: 0, dbData: makeDbNode('group1'),
			});
			graph.addNode('child1', {
				x: 50, y: 50, width: 200, height: 100, size: 15, color: '#fff',
				zIndex: 1, parentId: 'group1', dbData: makeDbNode('child1'),
			});
			store.set(graphAtom, graph);
			store.set(collapsedGroupsAtom, new Set(['group1']));

			const snapshot = exportGraph(store);

			expect(snapshot.groups).toHaveLength(1);
			expect(snapshot.groups[0].nodeId).toBe('child1');
			expect(snapshot.groups[0].parentId).toBe('group1');
			expect(snapshot.groups[0].isCollapsed).toBe(true);
		});
	});

	describe('importGraph', () => {
		it('imports nodes into empty store', () => {
			const store = createStore();
			const snapshot = createMinimalSnapshot({
				nodes: [{
					id: 'imported1',
					position: { x: 10, y: 20 },
					dimensions: { width: 200, height: 100 },
					size: 15, color: '#fff', zIndex: 0, label: 'Imported',
					dbData: makeDbNode('imported1', 10, 20),
				}],
			});

			importGraph(store, snapshot);

			const graph = store.get(graphAtom);
			expect(graph.hasNode('imported1')).toBe(true);
			expect(graph.getNodeAttribute('imported1', 'x')).toBe(10);
		});

		it('clears existing graph by default', () => {
			const store = createPopulatedStore();
			expect(store.get(graphAtom).order).toBe(2);

			importGraph(store, createMinimalSnapshot({
				nodes: [{ id: 'new1', position: { x: 0, y: 0 }, dimensions: { width: 100, height: 50 },
					size: 10, color: '#000', zIndex: 0, dbData: makeDbNode('new1') }],
			}));

			const graph = store.get(graphAtom);
			expect(graph.order).toBe(1);
			expect(graph.hasNode('new1')).toBe(true);
			expect(graph.hasNode('n1')).toBe(false);
		});

		it('respects clearExisting: false', () => {
			const store = createPopulatedStore();
			const existingCount = store.get(graphAtom).order;

			importGraph(store, createMinimalSnapshot({
				nodes: [{ id: 'new1', position: { x: 0, y: 0 }, dimensions: { width: 100, height: 50 },
					size: 10, color: '#000', zIndex: 0, dbData: makeDbNode('new1') }],
			}), { clearExisting: false });

			expect(store.get(graphAtom).order).toBe(existingCount + 1);
		});

		it('applies position offset', () => {
			const store = createStore();
			importGraph(store, createMinimalSnapshot({
				nodes: [{ id: 'n1', position: { x: 100, y: 200 }, dimensions: { width: 200, height: 100 },
					size: 15, color: '#fff', zIndex: 0, dbData: makeDbNode('n1') }],
			}), { offsetPosition: { x: 50, y: -100 } });

			const graph = store.get(graphAtom);
			expect(graph.getNodeAttribute('n1', 'x')).toBe(150);
			expect(graph.getNodeAttribute('n1', 'y')).toBe(100);
		});

		it('remaps IDs when remapIds is true', () => {
			const store = createStore();
			importGraph(store, createMinimalSnapshot({
				nodes: [
					{ id: 'n1', position: { x: 0, y: 0 }, dimensions: { width: 100, height: 50 },
						size: 10, color: '#000', zIndex: 0, dbData: makeDbNode('n1') },
					{ id: 'n2', position: { x: 200, y: 0 }, dimensions: { width: 100, height: 50 },
						size: 10, color: '#000', zIndex: 0, dbData: makeDbNode('n2') },
				],
				edges: [{ key: 'e1', sourceId: 'n1', targetId: 'n2',
					attributes: { type: 'solid' },
					dbData: makeDbEdge('e1', 'n1', 'n2') }],
			}), { remapIds: true });

			const graph = store.get(graphAtom);
			// Original IDs should not exist
			expect(graph.hasNode('n1')).toBe(false);
			expect(graph.hasNode('n2')).toBe(false);
			// But 2 nodes and 1 edge should exist with new UUIDs
			expect(graph.order).toBe(2);
			expect(graph.size).toBe(1);
		});

		it('restores viewport', () => {
			const store = createStore();
			importGraph(store, createMinimalSnapshot({
				viewport: { zoom: 2.5, pan: { x: -100, y: 200 } },
			}));

			expect(store.get(zoomAtom)).toBe(2.5);
			expect(store.get(panAtom)).toEqual({ x: -100, y: 200 });
		});

		it('restores collapsed groups', () => {
			const store = createStore();
			importGraph(store, createMinimalSnapshot({
				nodes: [
					{ id: 'g1', position: { x: 0, y: 0 }, dimensions: { width: 500, height: 400 },
						size: 15, color: '#ccc', zIndex: 0, dbData: makeDbNode('g1') },
					{ id: 'c1', position: { x: 50, y: 50 }, dimensions: { width: 200, height: 100 },
						size: 15, color: '#fff', zIndex: 1, parentId: 'g1', dbData: makeDbNode('c1') },
				],
				groups: [{ nodeId: 'c1', parentId: 'g1', isCollapsed: true }],
			}));

			expect(store.get(collapsedGroupsAtom).has('g1')).toBe(true);
		});

		it('skips edges whose endpoints are missing', () => {
			const store = createStore();
			importGraph(store, createMinimalSnapshot({
				nodes: [{ id: 'n1', position: { x: 0, y: 0 }, dimensions: { width: 100, height: 50 },
					size: 10, color: '#000', zIndex: 0, dbData: makeDbNode('n1') }],
				edges: [{ key: 'e1', sourceId: 'n1', targetId: 'missing',
					attributes: {}, dbData: makeDbEdge('e1', 'n1', 'missing') }],
			}));

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

	describe('round-trip', () => {
		it('export → import → export produces equivalent snapshot', () => {
			const store = createPopulatedStore();
			const snapshot1 = exportGraph(store);

			// Import into a fresh store
			const store2 = createStore();
			importGraph(store2, snapshot1);
			const snapshot2 = exportGraph(store2);

			// Compare structural content (ignore exportedAt timestamp)
			expect(snapshot2.nodes).toHaveLength(snapshot1.nodes.length);
			expect(snapshot2.edges).toHaveLength(snapshot1.edges.length);
			expect(snapshot2.viewport).toEqual(snapshot1.viewport);

			// Node IDs should match
			const ids1 = snapshot1.nodes.map(n => n.id).sort();
			const ids2 = snapshot2.nodes.map(n => n.id).sort();
			expect(ids2).toEqual(ids1);
		});
	});

	describe('validateSnapshot', () => {
		it('validates correct snapshot', () => {
			const result = validateSnapshot(createMinimalSnapshot());
			expect(result.valid).toBe(true);
			expect(result.errors).toHaveLength(0);
		});

		it('rejects null', () => {
			const result = validateSnapshot(null);
			expect(result.valid).toBe(false);
			expect(result.errors[0]).toContain('non-null object');
		});

		it('rejects wrong version', () => {
			const result = validateSnapshot({ ...createMinimalSnapshot(), version: 99 });
			expect(result.valid).toBe(false);
			expect(result.errors[0]).toContain('version');
		});

		it('rejects missing exportedAt', () => {
			const snap = createMinimalSnapshot();
			delete (snap as any).exportedAt;
			const result = validateSnapshot(snap);
			expect(result.valid).toBe(false);
			expect(result.errors[0]).toContain('exportedAt');
		});

		it('rejects missing nodes array', () => {
			const snap = createMinimalSnapshot();
			delete (snap as any).nodes;
			const result = validateSnapshot(snap);
			expect(result.valid).toBe(false);
		});

		it('rejects malformed node (missing id)', () => {
			const result = validateSnapshot(createMinimalSnapshot({
				nodes: [{ position: { x: 0, y: 0 }, dimensions: { width: 100, height: 50 } } as any],
			}));
			expect(result.valid).toBe(false);
			expect(result.errors.some(e => e.includes('nodes[0]'))).toBe(true);
		});

		it('rejects malformed edge (missing key)', () => {
			const result = validateSnapshot(createMinimalSnapshot({
				edges: [{ sourceId: 'a', targetId: 'b' } as any],
			}));
			expect(result.valid).toBe(false);
			expect(result.errors.some(e => e.includes('edges[0]'))).toBe(true);
		});

		it('rejects missing viewport', () => {
			const snap = createMinimalSnapshot();
			delete (snap as any).viewport;
			const result = validateSnapshot(snap);
			expect(result.valid).toBe(false);
		});

		it('rejects viewport missing zoom', () => {
			const result = validateSnapshot(createMinimalSnapshot({
				viewport: { pan: { x: 0, y: 0 } } as any,
			}));
			expect(result.valid).toBe(false);
			expect(result.errors.some(e => e.includes('zoom'))).toBe(true);
		});
	});
});
