import { describe, it, expect } from 'vitest';
import { deserialize, serialize } from './mentionSerializer';
import type { PersonMention } from '$lib/shared/types';

// ─── deserialize ─────────────────────────────────────────────────────────────

describe('deserialize', () => {
	it('returns a valid Tiptap doc for an empty string', () => {
		const doc = deserialize('', []);
		expect(doc.type).toBe('doc');
		expect(doc.content).toHaveLength(1);
		expect(doc.content![0].type).toBe('paragraph');
	});

	it('returns plain text node for text with no mentions', () => {
		const doc = deserialize('Hallo Welt', []);
		const para = doc.content![0];
		expect(para.content).toHaveLength(1);
		expect(para.content![0]).toEqual({ type: 'text', text: 'Hallo Welt' });
	});

	it('places a mention node at the correct position in the paragraph', () => {
		const sidecar: PersonMention[] = [{ personId: 'uuid-x', displayName: 'Clara' }];
		const doc = deserialize('Hallo @Clara Welt', sidecar);
		const nodes = doc.content![0].content!;
		expect(nodes).toHaveLength(3);
		expect(nodes[0]).toEqual({ type: 'text', text: 'Hallo ' });
		expect(nodes[1]).toMatchObject({
			type: 'mention',
			attrs: { displayName: 'Clara', personId: 'uuid-x' }
		});
		expect(nodes[2]).toEqual({ type: 'text', text: ' Welt' });
	});

	it('prefers the longer displayName when two sidecar entries share a prefix', () => {
		const sidecar: PersonMention[] = [
			{ personId: 'uuid-short', displayName: 'Auguste' },
			{ personId: 'uuid-long', displayName: 'Auguste Raddatz' }
		];
		const doc = deserialize('@Auguste Raddatz', sidecar);
		const nodes = doc.content![0].content!;
		expect(nodes).toHaveLength(1);
		expect(nodes[0]).toMatchObject({
			type: 'mention',
			attrs: { personId: 'uuid-long', displayName: 'Auguste Raddatz' }
		});
	});

	it('splits multiple paragraphs on newline', () => {
		const doc = deserialize('Zeile 1\nZeile 2', []);
		expect(doc.content).toHaveLength(2);
		expect(doc.content![0].content![0]).toEqual({ type: 'text', text: 'Zeile 1' });
		expect(doc.content![1].content![0]).toEqual({ type: 'text', text: 'Zeile 2' });
	});
});

// ─── serialize ───────────────────────────────────────────────────────────────

describe('serialize', () => {
	it('serializes plain text unchanged', () => {
		const doc = deserialize('Hallo Welt', []);
		const { text, mentionedPersons } = serialize(doc);
		expect(text).toBe('Hallo Welt');
		expect(mentionedPersons).toEqual([]);
	});

	it('serializes mention node back to @displayName', () => {
		const sidecar: PersonMention[] = [{ personId: 'uuid-x', displayName: 'Clara' }];
		const doc = deserialize('Hallo @Clara Welt', sidecar);
		const { text, mentionedPersons } = serialize(doc);
		expect(text).toBe('Hallo @Clara Welt');
		expect(mentionedPersons).toEqual([{ personId: 'uuid-x', displayName: 'Clara' }]);
	});

	it('joins multi-paragraph doc back with newlines', () => {
		const doc = deserialize('Zeile 1\nZeile 2', []);
		const { text } = serialize(doc);
		expect(text).toBe('Zeile 1\nZeile 2');
	});

	it('de-duplicates repeated mentions in the sidecar', () => {
		const sidecar: PersonMention[] = [{ personId: 'uuid-x', displayName: 'Clara' }];
		const doc = deserialize('@Clara und @Clara', sidecar);
		const { mentionedPersons } = serialize(doc);
		expect(mentionedPersons).toHaveLength(1);
		expect(mentionedPersons[0].personId).toBe('uuid-x');
	});
});

// ─── Round-trip invariant ─────────────────────────────────────────────────────

describe('round-trip invariant', () => {
	it('text is preserved exactly through deserialize → serialize', () => {
		const cases = [
			['', []],
			['Hallo Welt', []],
			['@Clara schreibt', [{ personId: 'uuid-x', displayName: 'Clara' }]],
			['Zeile 1\nZeile 2', []],
			['Sehr geehrte @Auguste Raddatz,', [{ personId: 'uuid-aug', displayName: 'Auguste Raddatz' }]]
		] as const;

		for (const [text, sidecar] of cases) {
			const doc = deserialize(text, sidecar as PersonMention[]);
			const { text: out } = serialize(doc);
			expect(out, `round-trip failed for: "${text}"`).toBe(text);
		}
	});
});

// ─── Backward compatibility (AC-6) ────────────────────────────────────────────

describe('backward compatibility', () => {
	it('old-format full-name sidecar entry still round-trips correctly', () => {
		// Before this issue, displayName stored the person's full DB name.
		// renderTranscriptionBody already handles this — so does the serializer.
		const oldSidecar: PersonMention[] = [{ personId: 'uuid-aug', displayName: 'Auguste Raddatz' }];
		const text = 'Brief von @Auguste Raddatz';
		const doc = deserialize(text, oldSidecar);
		const { text: out, mentionedPersons } = serialize(doc);
		expect(out).toBe(text);
		expect(mentionedPersons).toEqual(oldSidecar);
	});
});

// ─── Security ─────────────────────────────────────────────────────────────────

describe('security', () => {
	it('displayName containing HTML special chars is preserved as a string, not injected', () => {
		const sidecar: PersonMention[] = [
			{ personId: 'uuid-x', displayName: '<script>alert(1)</script>' }
		];
		const text = '@<script>alert(1)</script>';
		const doc = deserialize(text, sidecar);
		const { mentionedPersons } = serialize(doc);
		// The displayName is stored verbatim — HTML escaping is the renderer's job
		expect(mentionedPersons[0].displayName).toBe('<script>alert(1)</script>');
	});
});
