/**
 * Adapter Conformance Test Harness
 *
 * Validate any CanvasStorageAdapter implementation against the full
 * contract. Consumers can import `runAdapterConformanceTests` and pass
 * their own adapter factory to run the same suite against custom backends.
 *
 * @since 1.4.0
 */

import { describe, it, expect, beforeEach } from 'vitest';
import type { CanvasStorageAdapter } from '../db/adapter';
import { InMemoryStorageAdapter } from '../db/adapter';

// =============================================================================
// Shared Test Suite
// =============================================================================

/**
 * Run the full adapter conformance suite against any CanvasStorageAdapter.
 *
 * @param name       Label for the test group
 * @param factory    Function that returns a fresh adapter instance per test
 *
 * @example
 * ```ts
 * import { runAdapterConformanceTests } from '@blinksgg/canvas/__tests__/adapter-conformance.test';
 * runAdapterConformanceTests('MyCustomAdapter', () => new MyCustomAdapter());
 * ```
 */
export function runAdapterConformanceTests(
	name: string,
	factory: () => CanvasStorageAdapter,
) {
	describe(`${name} — Adapter Conformance`, () => {
		let adapter: CanvasStorageAdapter;

		beforeEach(() => {
			adapter = factory();
		});

		// --- Node CRUD ---

		describe('node operations', () => {
			it('creates a node with generated id', async () => {
				const node = await adapter.createNode('g1', { label: 'Test', node_type: 'default' });
				expect(node.id).toBeDefined();
				expect(typeof node.id).toBe('string');
				expect(node.graph_id).toBe('g1');
				expect(node.label).toBe('Test');
			});

			it('creates a node with explicit id', async () => {
				const node = await adapter.createNode('g1', { id: 'custom-id', label: 'Explicit' });
				expect(node.id).toBe('custom-id');
			});

			it('fetches nodes filtered by graph ID', async () => {
				await adapter.createNode('g1', { label: 'A' });
				await adapter.createNode('g2', { label: 'B' });
				await adapter.createNode('g1', { label: 'C' });

				const g1Nodes = await adapter.fetchNodes('g1');
				expect(g1Nodes).toHaveLength(2);

				const g2Nodes = await adapter.fetchNodes('g2');
				expect(g2Nodes).toHaveLength(1);

				const g3Nodes = await adapter.fetchNodes('g3');
				expect(g3Nodes).toHaveLength(0);
			});

			it('updates a node', async () => {
				const node = await adapter.createNode('g1', { label: 'Before' });
				const updated = await adapter.updateNode(node.id, { label: 'After' });

				expect(updated.label).toBe('After');
				expect(updated.id).toBe(node.id);

				const fetched = await adapter.fetchNodes('g1');
				expect(fetched[0].label).toBe('After');
			});

			it('throws on updating non-existent node', async () => {
				await expect(adapter.updateNode('nonexistent', { label: 'X' })).rejects.toThrow();
			});

			it('deletes a node', async () => {
				const node = await adapter.createNode('g1', { label: 'ToDelete' });
				await adapter.deleteNode(node.id);
				expect(await adapter.fetchNodes('g1')).toHaveLength(0);
			});

			it('cascade deletes connected edges when node is deleted', async () => {
				const n1 = await adapter.createNode('g1', { label: 'A' });
				const n2 = await adapter.createNode('g1', { label: 'B' });
				await adapter.createEdge('g1', { source_node_id: n1.id, target_node_id: n2.id });

				await adapter.deleteNode(n1.id);

				expect(await adapter.fetchNodes('g1')).toHaveLength(1);
				expect(await adapter.fetchEdges('g1')).toHaveLength(0);
			});
		});

		// --- Edge CRUD ---

		describe('edge operations', () => {
			it('creates an edge with generated id', async () => {
				const edge = await adapter.createEdge('g1', {
					source_node_id: 'n1',
					target_node_id: 'n2',
					edge_type: 'flow',
				});

				expect(edge.id).toBeDefined();
				expect(edge.source_node_id).toBe('n1');
				expect(edge.target_node_id).toBe('n2');
				expect(edge.edge_type).toBe('flow');
			});

			it('fetches edges filtered by graph ID', async () => {
				await adapter.createEdge('g1', { source_node_id: 'a', target_node_id: 'b' });
				await adapter.createEdge('g2', { source_node_id: 'c', target_node_id: 'd' });

				expect(await adapter.fetchEdges('g1')).toHaveLength(1);
				expect(await adapter.fetchEdges('g2')).toHaveLength(1);
				expect(await adapter.fetchEdges('g3')).toHaveLength(0);
			});

			it('updates an edge', async () => {
				const edge = await adapter.createEdge('g1', { source_node_id: 'n1', target_node_id: 'n2' });
				const updated = await adapter.updateEdge(edge.id, { edge_type: 'data' });
				expect(updated.edge_type).toBe('data');
			});

			it('throws on updating non-existent edge', async () => {
				await expect(adapter.updateEdge('nonexistent', { edge_type: 'X' })).rejects.toThrow();
			});

			it('deletes an edge', async () => {
				const edge = await adapter.createEdge('g1', { source_node_id: 'n1', target_node_id: 'n2' });
				await adapter.deleteEdge(edge.id);
				expect(await adapter.fetchEdges('g1')).toHaveLength(0);
			});
		});

		// --- Batch Operations ---

		describe('batch operations', () => {
			it('createNodes creates multiple nodes', async () => {
				if (!adapter.createNodes) return; // skip if not implemented
				const nodes = await adapter.createNodes('g1', [
					{ label: 'A' },
					{ label: 'B' },
					{ label: 'C' },
				]);
				expect(nodes).toHaveLength(3);
				expect(await adapter.fetchNodes('g1')).toHaveLength(3);
			});

			it('deleteNodes removes multiple nodes', async () => {
				if (!adapter.deleteNodes) return;
				const n1 = await adapter.createNode('g1', { label: 'A' });
				const n2 = await adapter.createNode('g1', { label: 'B' });
				await adapter.createNode('g1', { label: 'C' });

				await adapter.deleteNodes([n1.id, n2.id]);
				expect(await adapter.fetchNodes('g1')).toHaveLength(1);
			});
		});
	});
}

// =============================================================================
// Run against InMemoryStorageAdapter
// =============================================================================

runAdapterConformanceTests('InMemoryStorageAdapter', () => new InMemoryStorageAdapter());
