import { hasWhiteboardFence, parseWhiteboard } from './parse';
import { newWhiteboardBody, serializeWhiteboard } from './serialize';
import { isInternalRef, RefKind, resolveFileRef } from './resolveRef';
import { Canvas } from './jsoncanvas';

const sampleCanvas: Canvas = {
	nodes: [
		{ id: 'a', type: 'text', x: 0, y: 0, width: 200, height: 100, text: 'Hello' },
		{ id: 'b', type: 'file', x: 300, y: 0, width: 200, height: 100, file: ':/0123456789abcdef0123456789abcdef' },
		{ id: 'c', type: 'link', x: 0, y: 200, width: 200, height: 100, url: 'https://example.com' },
		{ id: 'g', type: 'group', x: -50, y: -50, width: 600, height: 400, label: 'Phase 1' },
	],
	edges: [
		{ id: 'e1', fromNode: 'a', fromSide: 'right', toNode: 'b', toSide: 'left', label: 'depends on' },
	],
};

describe('whiteboard format module', () => {
	test('parses a body with a jsoncanvas fence and preserves surrounding text', () => {
		const before = '# My note\n\nSome intro text.\n\n';
		const after = '\nA closing paragraph.\n';
		const body = `${before}\`\`\`jsoncanvas\n${JSON.stringify(sampleCanvas)}\n\`\`\`${after}`;

		const result = parseWhiteboard(body);

		expect(result.hasCanvas).toBe(true);
		expect(result.prefix).toBe(before);
		expect(result.suffix).toBe(after.replace(/^\n/, '')); // leading newline is consumed by the fence terminator
		expect(result.canvas.nodes).toHaveLength(4);
		expect(result.canvas.edges).toHaveLength(1);
	});

	test('hasWhiteboardFence detects fence presence', () => {
		expect(hasWhiteboardFence('plain note')).toBe(false);
		expect(hasWhiteboardFence('```jsoncanvas\n{}\n```')).toBe(true);
		expect(hasWhiteboardFence('# title\n\n```jsoncanvas\n{}\n```\n')).toBe(true);
	});

	test('returns empty canvas with hasCanvas=false when fence content is invalid JSON', () => {
		const body = '```jsoncanvas\nthis is not json\n```';
		const result = parseWhiteboard(body);
		expect(result.hasCanvas).toBe(false);
		expect(result.canvas.nodes).toHaveLength(0);
		expect(result.canvas.edges).toHaveLength(0);
		expect(result.parseError).toMatch(/^Invalid JSON: /);
	});

	test('reports a schema error when the JSON is valid but not a JSONCanvas object', () => {
		const body = '```jsoncanvas\n[]\n```';
		const result = parseWhiteboard(body);
		expect(result.hasCanvas).toBe(false);
		expect(result.parseError).toMatch(/JSONCanvas/);
	});

	test('parseError is null when there is no fence at all', () => {
		expect(parseWhiteboard('').parseError).toBeNull();
		expect(parseWhiteboard('plain note').parseError).toBeNull();
	});

	test('drops invalid nodes and edges that reference unknown nodes', () => {
		const dirty = {
			nodes: [
				{ id: 'a', type: 'text', x: 0, y: 0, width: 100, height: 100, text: 'ok' },
				{ id: 'b', type: 'text', x: 0, y: 0 }, // missing dimensions
				{ type: 'text', x: 0, y: 0, width: 100, height: 100, text: 'no id' },
			],
			edges: [
				{ id: 'e1', fromNode: 'a', toNode: 'a' },
				{ id: 'e2', fromNode: 'a', toNode: 'ghost' },
			],
		};
		const body = `\`\`\`jsoncanvas\n${JSON.stringify(dirty)}\n\`\`\``;
		const result = parseWhiteboard(body);
		expect(result.canvas.nodes.map(n => n.id)).toEqual(['a']);
		expect(result.canvas.edges.map(e => e.id)).toEqual(['e1']);
	});

	test('round-trips: serialize then parse yields the same canvas', () => {
		const body = serializeWhiteboard('', sampleCanvas);
		const result = parseWhiteboard(body);
		expect(result.hasCanvas).toBe(true);
		expect(result.canvas).toEqual(sampleCanvas);
	});

	test('serialize replaces an existing fence in place and preserves prefix/suffix', () => {
		const original = `Intro\n\n\`\`\`jsoncanvas\n${JSON.stringify({ nodes: [], edges: [] })}\n\`\`\`\n\nOutro\n`;
		const updated = serializeWhiteboard(original, sampleCanvas);
		const result = parseWhiteboard(updated);
		expect(result.canvas).toEqual(sampleCanvas);
		expect(result.prefix).toBe('Intro\n\n');
		expect(updated.endsWith('Outro\n')).toBe(true);
	});

	test('serialize appends a fence when none exists', () => {
		const before = '# Title\n\nSome content.';
		const updated = serializeWhiteboard(before, sampleCanvas);
		expect(updated.startsWith(before)).toBe(true);
		expect(hasWhiteboardFence(updated)).toBe(true);
	});

	test('newWhiteboardBody produces a parseable empty canvas', () => {
		const body = newWhiteboardBody();
		const result = parseWhiteboard(body);
		expect(result.hasCanvas).toBe(true);
		expect(result.canvas.nodes).toEqual([]);
		expect(result.canvas.edges).toEqual([]);
	});

	test('round-trips edge fromEnd / toEnd / sides without losing or flipping them', () => {
		const canvas: Canvas = {
			nodes: [
				{ id: 'a', type: 'text', x: 0, y: 0, width: 100, height: 100, text: 'a' },
				{ id: 'b', type: 'text', x: 200, y: 0, width: 100, height: 100, text: 'b' },
			],
			edges: [
				{ id: 'forward', fromNode: 'a', toNode: 'b', toEnd: 'arrow' },
				{ id: 'backward', fromNode: 'a', toNode: 'b', fromEnd: 'arrow', toEnd: 'none' },
				{ id: 'both', fromNode: 'a', toNode: 'b', fromEnd: 'arrow', toEnd: 'arrow' },
				{ id: 'sided', fromNode: 'a', toNode: 'b', fromSide: 'right', toSide: 'left' },
			],
		};
		const body = serializeWhiteboard('', canvas);
		const result = parseWhiteboard(body);
		expect(result.hasCanvas).toBe(true);
		expect(result.canvas.edges).toEqual(canvas.edges);
	});

	test('preserves group nodes through parse + serialize', () => {
		// Joplin's editor doesn't render group nodes but must not silently
		// drop them on save — other tools may contain them.
		const canvas: Canvas = {
			nodes: [
				{ id: 'g', type: 'group', x: -50, y: -50, width: 400, height: 300, label: 'Phase 1', background: '#eee' },
				{ id: 't', type: 'text', x: 0, y: 0, width: 100, height: 60, text: 'inside' },
			],
			edges: [],
		};
		const body = serializeWhiteboard('', canvas);
		const result = parseWhiteboard(body);
		expect(result.hasCanvas).toBe(true);
		expect(result.canvas.nodes).toEqual(canvas.nodes);
	});

	test.each([
		[':/0123456789abcdef0123456789abcdef', RefKind.Resource, '0123456789abcdef0123456789abcdef'],
		['https://example.com/foo.pdf', RefKind.External, 'https://example.com/foo.pdf'],
		['Notes/Foo.md', RefKind.External, 'Notes/Foo.md'],
	])('resolveFileRef classifies %s', (value, kind, id) => {
		const r = resolveFileRef(value);
		expect(r.kind).toBe(kind);
		expect(r.id).toBe(id);
	});

	test('resolveFileRef respects kindHint for internal refs', () => {
		const r = resolveFileRef(':/0123456789abcdef0123456789abcdef', RefKind.Note);
		expect(r.kind).toBe(RefKind.Note);
	});

	test('isInternalRef matches only proper :/ id strings', () => {
		expect(isInternalRef(':/0123456789abcdef0123456789abcdef')).toBe(true);
		expect(isInternalRef(':/short')).toBe(false);
		expect(isInternalRef('not internal')).toBe(false);
	});
});
