/**
 * Test: Automerge round-trip — create, save, load, sync, verify.
 *
 * Exercises the full local-first stack:
 *   1. SyncServer (in-memory doc management)
 *   2. Doc persistence (save to disk + load from disk)
 *   3. Schema init factories (NotebookDoc, BoardDoc, etc.)
 *   4. Doc change + onDocChange callback
 *
 * Usage:  bun run scripts/test-automerge-roundtrip.ts
 */

// Must set env BEFORE imports (doc-persistence reads it at module level)
const TEST_DIR = '/tmp/rspace-automerge-test';
process.env.DOCS_STORAGE_DIR = TEST_DIR;

import * as Automerge from '@automerge/automerge';
import { mkdirSync, rmSync, existsSync, readdirSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { SyncServer } from '../server/local-first/sync-server';
import { docIdToPath, saveDoc, loadAllDocs } from '../server/local-first/doc-persistence';

// Cleanup from previous runs
if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true });
mkdirSync(TEST_DIR, { recursive: true });

let passed = 0;
let failed = 0;

function assert(condition: boolean, label: string) {
	if (condition) {
		console.log(`  ✓ ${label}`);
		passed++;
	} else {
		console.error(`  ✗ ${label}`);
		failed++;
	}
}

// ─── Test 1: docIdToPath mapping ──────────────────────────

console.log('\n── Test 1: docId ↔ path mapping ──');
{
	const path = docIdToPath('demo:notes:notebooks:abc');
	assert(path.endsWith('/demo/notes/notebooks/abc.automerge'), `docIdToPath → ${path}`);

	const path2 = docIdToPath('myspace:work:boards:board-1');
	assert(path2.endsWith('/myspace/work/boards/board-1.automerge'), `docIdToPath boards → ${path2}`);

	let threw = false;
	try { docIdToPath('invalid'); } catch { threw = true; }
	assert(threw, 'docIdToPath rejects invalid docId (< 3 parts)');
}

// ─── Test 2: SyncServer in-memory CRUD ─────────────────────

console.log('\n── Test 2: SyncServer in-memory CRUD ──');
{
	interface TestDoc { title: string; items: Record<string, { text: string }> }

	const docChanges: string[] = [];
	const server = new SyncServer({
		participantMode: true,
		onDocChange: (docId) => docChanges.push(docId),
	});

	// Create a doc
	let doc = Automerge.init<TestDoc>();
	doc = Automerge.change(doc, 'init', (d) => {
		d.title = 'Test Notebook';
		d.items = {};
	});
	server.setDoc('test:notes:notebooks:nb1', doc);

	assert(server.getDocIds().includes('test:notes:notebooks:nb1'), 'setDoc registers docId');

	// Read it back
	const loaded = server.getDoc<TestDoc>('test:notes:notebooks:nb1');
	assert(loaded !== undefined, 'getDoc returns the doc');
	assert(loaded!.title === 'Test Notebook', 'doc content preserved');

	// Change via changeDoc
	const changed = server.changeDoc<TestDoc>('test:notes:notebooks:nb1', 'add item', (d) => {
		d.items['item-1'] = { text: 'Hello local-first' };
	});
	assert(changed !== null, 'changeDoc returns updated doc');
	assert(changed!.items['item-1'].text === 'Hello local-first', 'changeDoc content correct');
	assert(docChanges.length === 1, 'onDocChange callback fired');

	// Verify the server's copy is updated
	const reloaded = server.getDoc<TestDoc>('test:notes:notebooks:nb1');
	assert(reloaded!.items['item-1'].text === 'Hello local-first', 'server copy updated after changeDoc');
}

// ─── Test 3: Relay mode ────────────────────────────────────

console.log('\n── Test 3: Relay mode (encrypted spaces) ──');
{
	const server = new SyncServer({ participantMode: true });

	assert(!server.isRelayOnly('demo:notes:notebooks:x'), 'not relay by default');

	server.setRelayOnly('encrypted-space', true);
	assert(server.isRelayOnly('encrypted-space'), 'exact match → relay');
	assert(server.isRelayOnly('encrypted-space:notes:notebooks:x'), 'prefix match → relay');
	assert(!server.isRelayOnly('other-space:notes:notebooks:x'), 'other space → not relay');

	server.setRelayOnly('encrypted-space', false);
	assert(!server.isRelayOnly('encrypted-space:notes:notebooks:x'), 'after removal → not relay');
}

// ─── Test 4: Disk persistence round-trip ───────────────────
// Note: We test Automerge binary serialization directly rather than using
// doc-persistence (which reads DOCS_STORAGE_DIR at module load time).

console.log('\n── Test 4: Disk persistence round-trip ──');
await (async () => {
	interface NoteDoc { title: string; content: string }

	// Create a doc
	let doc = Automerge.init<NoteDoc>();
	doc = Automerge.change(doc, 'init', (d) => {
		d.title = 'Persistent Note';
		d.content = 'This should survive a restart';
	});

	// Serialize to binary
	const binary = Automerge.save(doc);
	assert(binary.byteLength > 0, `Automerge.save produces ${binary.byteLength} bytes`);

	// Write to temp dir
	const { mkdir: mk, writeFile: wf, readFile: rf } = await import('node:fs/promises');
	const { dirname: dn } = await import('node:path');
	const filePath = resolve(TEST_DIR, 'roundtrip/notes/notebooks/persist-1.automerge');
	await mk(dn(filePath), { recursive: true });
	await wf(filePath, binary);
	assert(existsSync(filePath), `file written to disk`);

	// Read back and deserialize
	const rawBuf = await rf(filePath);
	const reloaded = Automerge.load<NoteDoc>(new Uint8Array(rawBuf));
	assert(reloaded.title === 'Persistent Note', 'title preserved after load');
	assert(reloaded.content === 'This should survive a restart', 'content preserved after load');

	// Verify Automerge history
	const history = Automerge.getHistory(reloaded);
	assert(history.length === 1, `history has ${history.length} change(s)`);

	// Test: modify, save again, load again
	const doc2 = Automerge.change(reloaded, 'update', (d) => {
		d.content = 'Updated content after reload';
	});
	const binary2 = Automerge.save(doc2);
	await wf(filePath, binary2);
	const rawBuf2 = await rf(filePath);
	const reloaded2 = Automerge.load<NoteDoc>(new Uint8Array(rawBuf2));
	assert(reloaded2.content === 'Updated content after reload', 'content updated after second save/load');
	assert(Automerge.getHistory(reloaded2).length === 2, 'history has 2 changes after update');

	// Load via loadAllDocs into a SyncServer (uses TEST_DIR since that's what's on disk)
	// We do this by creating a SyncServer and loading manually
	const server2 = new SyncServer({ participantMode: true });
	const raw = await rf(filePath);
	const loadedDoc = Automerge.load<NoteDoc>(new Uint8Array(raw));
	server2.setDoc('roundtrip:notes:notebooks:persist-1', loadedDoc);
	const fromServer = server2.getDoc<NoteDoc>('roundtrip:notes:notebooks:persist-1');
	assert(fromServer!.title === 'Persistent Note', 'SyncServer holds correct doc from disk');
})();

// ─── Test 5: Multiple docs + listDocs ──────────────────────

console.log('\n── Test 5: Multiple docs + listing ──');
await (async () => {
	const server = new SyncServer({ participantMode: true });

	for (const id of ['space-a:work:boards:b1', 'space-a:work:boards:b2', 'space-b:cal:events']) {
		let doc = Automerge.init<{ label: string }>();
		doc = Automerge.change(doc, 'init', (d) => { d.label = id; });
		server.setDoc(id, doc);
	}

	const ids = server.getDocIds();
	assert(ids.length === 3, `3 docs registered (got ${ids.length})`);
	assert(ids.includes('space-a:work:boards:b1'), 'board b1 listed');
	assert(ids.includes('space-b:cal:events'), 'cal events listed');
})();

// ─── Test 6: Peer subscribe + sync message flow ────────────

console.log('\n── Test 6: Peer subscribe + sync flow ──');
{
	interface SimpleDoc { value: number }

	const sent: Array<{ peerId: string; msg: string }> = [];
	const server = new SyncServer({ participantMode: true });

	// Create a doc on the server
	let doc = Automerge.init<SimpleDoc>();
	doc = Automerge.change(doc, 'set value', (d) => { d.value = 42; });
	server.setDoc('sync-test:data:metrics', doc);

	// Add a mock peer
	const mockWs = {
		send: (data: string) => sent.push({ peerId: 'peer-1', msg: data }),
		readyState: 1,
	};
	server.addPeer('peer-1', mockWs);

	// Subscribe peer to the doc
	server.handleMessage('peer-1', JSON.stringify({
		type: 'subscribe',
		docIds: ['sync-test:data:metrics'],
	}));

	assert(server.getDocSubscribers('sync-test:data:metrics').includes('peer-1'), 'peer subscribed');
	assert(sent.length > 0, `sync message sent to peer (${sent.length} message(s))`);

	// Verify the sync message is valid JSON with type 'sync'
	const firstMsg = JSON.parse(sent[0].msg);
	assert(firstMsg.type === 'sync', `message type is 'sync'`);
	assert(firstMsg.docId === 'sync-test:data:metrics', 'correct docId in sync message');
	assert(Array.isArray(firstMsg.data), 'sync data is array (Uint8Array serialized)');

	// Clean up peer
	server.removePeer('peer-1');
	assert(!server.getPeerIds().includes('peer-1'), 'peer removed');
	assert(server.getDocSubscribers('sync-test:data:metrics').length === 0, 'subscriber cleaned up');
}

// ─── Test 7: Ping/pong ────────────────────────────────────

console.log('\n── Test 7: Ping/pong ──');
{
	const sent: string[] = [];
	const server = new SyncServer({ participantMode: true });
	const mockWs = {
		send: (data: string) => sent.push(data),
		readyState: 1,
	};
	server.addPeer('ping-peer', mockWs);
	server.handleMessage('ping-peer', JSON.stringify({ type: 'ping' }));

	assert(sent.length === 1, 'pong sent');
	assert(JSON.parse(sent[0]).type === 'pong', 'response is pong');
	server.removePeer('ping-peer');
}

// ─── Summary ───────────────────────────────────────────────

console.log(`\n${'═'.repeat(50)}`);
console.log(`  ${passed} passed, ${failed} failed`);
console.log(`${'═'.repeat(50)}\n`);

// Cleanup
rmSync(TEST_DIR, { recursive: true });

process.exit(failed > 0 ? 1 : 0);
