1import { describe, it, expect } from 'vitest';
  2import { AttachmentType } from '$lib/enums';
  3import {
  4	formatMessageForClipboard,
  5	parseClipboardContent,
  6	hasClipboardAttachments
  7} from '$lib/utils/clipboard';
  8
  9describe('formatMessageForClipboard', () => {
 10	it('returns plain content when no extras', () => {
 11		const result = formatMessageForClipboard('Hello world', undefined);
 12		expect(result).toBe('Hello world');
 13	});
 14
 15	it('returns plain content when extras is empty array', () => {
 16		const result = formatMessageForClipboard('Hello world', []);
 17		expect(result).toBe('Hello world');
 18	});
 19
 20	it('handles empty string content', () => {
 21		const result = formatMessageForClipboard('', undefined);
 22		expect(result).toBe('');
 23	});
 24
 25	it('returns plain content when extras has only non-text attachments', () => {
 26		const extras = [
 27			{
 28				type: AttachmentType.IMAGE as const,
 29				name: 'image.png',
 30				base64Url: 'data:image/png;base64,...'
 31			}
 32		];
 33		const result = formatMessageForClipboard('Hello world', extras);
 34		expect(result).toBe('Hello world');
 35	});
 36
 37	it('filters non-text attachments and keeps only text ones', () => {
 38		const extras = [
 39			{
 40				type: AttachmentType.IMAGE as const,
 41				name: 'image.png',
 42				base64Url: 'data:image/png;base64,...'
 43			},
 44			{
 45				type: AttachmentType.TEXT as const,
 46				name: 'file.txt',
 47				content: 'Text content'
 48			},
 49			{
 50				type: AttachmentType.PDF as const,
 51				name: 'doc.pdf',
 52				base64Data: 'data:application/pdf;base64,...',
 53				content: 'PDF content',
 54				processedAsImages: false
 55			}
 56		];
 57		const result = formatMessageForClipboard('Hello', extras);
 58
 59		expect(result).toContain('"file.txt"');
 60		expect(result).not.toContain('image.png');
 61		expect(result).not.toContain('doc.pdf');
 62	});
 63
 64	it('formats message with text attachments', () => {
 65		const extras = [
 66			{
 67				type: AttachmentType.TEXT as const,
 68				name: 'file1.txt',
 69				content: 'File 1 content'
 70			},
 71			{
 72				type: AttachmentType.TEXT as const,
 73				name: 'file2.txt',
 74				content: 'File 2 content'
 75			}
 76		];
 77		const result = formatMessageForClipboard('Hello world', extras);
 78
 79		expect(result).toContain('"Hello world"');
 80		expect(result).toContain('"type": "TEXT"');
 81		expect(result).toContain('"name": "file1.txt"');
 82		expect(result).toContain('"content": "File 1 content"');
 83		expect(result).toContain('"name": "file2.txt"');
 84	});
 85
 86	it('handles content with quotes and special characters', () => {
 87		const content = 'Hello "world" with\nnewline';
 88		const extras = [
 89			{
 90				type: AttachmentType.TEXT as const,
 91				name: 'test.txt',
 92				content: 'Test content'
 93			}
 94		];
 95		const result = formatMessageForClipboard(content, extras);
 96
 97		// Should be valid JSON
 98		expect(result.startsWith('"')).toBe(true);
 99		// The content should be properly escaped
100		const parsed = JSON.parse(result.split('\n')[0]);
101		expect(parsed).toBe(content);
102	});
103
104	it('converts legacy context type to TEXT type', () => {
105		const extras = [
106			{
107				type: AttachmentType.LEGACY_CONTEXT as const,
108				name: 'legacy.txt',
109				content: 'Legacy content'
110			}
111		];
112		const result = formatMessageForClipboard('Hello', extras);
113
114		expect(result).toContain('"type": "TEXT"');
115		expect(result).not.toContain('"context"');
116	});
117
118	it('handles attachment content with special characters', () => {
119		const extras = [
120			{
121				type: AttachmentType.TEXT as const,
122				name: 'code.js',
123				content: 'const x = "hello\\nworld";\nconst y = `template ${var}`;'
124			}
125		];
126		const formatted = formatMessageForClipboard('Check this code', extras);
127		const parsed = parseClipboardContent(formatted);
128
129		expect(parsed.textAttachments[0].content).toBe(
130			'const x = "hello\\nworld";\nconst y = `template ${var}`;'
131		);
132	});
133
134	it('handles unicode characters in content and attachments', () => {
135		const extras = [
136			{
137				type: AttachmentType.TEXT as const,
138				name: 'unicode.txt',
139				content: '日本語テスト 🎉 émojis'
140			}
141		];
142		const formatted = formatMessageForClipboard('Привет мир 👋', extras);
143		const parsed = parseClipboardContent(formatted);
144
145		expect(parsed.message).toBe('Привет мир 👋');
146		expect(parsed.textAttachments[0].content).toBe('日本語テスト 🎉 émojis');
147	});
148
149	it('formats as plain text when asPlainText is true', () => {
150		const extras = [
151			{
152				type: AttachmentType.TEXT as const,
153				name: 'file1.txt',
154				content: 'File 1 content'
155			},
156			{
157				type: AttachmentType.TEXT as const,
158				name: 'file2.txt',
159				content: 'File 2 content'
160			}
161		];
162		const result = formatMessageForClipboard('Hello world', extras, true);
163
164		expect(result).toBe('Hello world\n\nFile 1 content\n\nFile 2 content');
165	});
166
167	it('returns plain content when asPlainText is true but no attachments', () => {
168		const result = formatMessageForClipboard('Hello world', [], true);
169		expect(result).toBe('Hello world');
170	});
171
172	it('plain text mode does not use JSON format', () => {
173		const extras = [
174			{
175				type: AttachmentType.TEXT as const,
176				name: 'test.txt',
177				content: 'Test content'
178			}
179		];
180		const result = formatMessageForClipboard('Hello', extras, true);
181
182		expect(result).not.toContain('"type"');
183		expect(result).not.toContain('[');
184		expect(result).toBe('Hello\n\nTest content');
185	});
186});
187
188describe('parseClipboardContent', () => {
189	it('returns plain text as message when not in special format', () => {
190		const result = parseClipboardContent('Hello world');
191
192		expect(result.message).toBe('Hello world');
193		expect(result.textAttachments).toHaveLength(0);
194	});
195
196	it('handles empty string input', () => {
197		const result = parseClipboardContent('');
198
199		expect(result.message).toBe('');
200		expect(result.textAttachments).toHaveLength(0);
201	});
202
203	it('handles whitespace-only input', () => {
204		const result = parseClipboardContent('   \n\t  ');
205
206		expect(result.message).toBe('   \n\t  ');
207		expect(result.textAttachments).toHaveLength(0);
208	});
209
210	it('returns plain text as message when starts with quote but invalid format', () => {
211		const result = parseClipboardContent('"Unclosed quote');
212
213		expect(result.message).toBe('"Unclosed quote');
214		expect(result.textAttachments).toHaveLength(0);
215	});
216
217	it('returns original text when JSON array is malformed', () => {
218		const input = '"Hello"\n[invalid json';
219
220		const result = parseClipboardContent(input);
221
222		expect(result.message).toBe('"Hello"\n[invalid json');
223		expect(result.textAttachments).toHaveLength(0);
224	});
225
226	it('parses message with text attachments', () => {
227		const input = `"Hello world"
228[
229  {"type":"TEXT","name":"file1.txt","content":"File 1 content"},
230  {"type":"TEXT","name":"file2.txt","content":"File 2 content"}
231]`;
232
233		const result = parseClipboardContent(input);
234
235		expect(result.message).toBe('Hello world');
236		expect(result.textAttachments).toHaveLength(2);
237		expect(result.textAttachments[0].name).toBe('file1.txt');
238		expect(result.textAttachments[0].content).toBe('File 1 content');
239		expect(result.textAttachments[1].name).toBe('file2.txt');
240		expect(result.textAttachments[1].content).toBe('File 2 content');
241	});
242
243	it('handles escaped quotes in message', () => {
244		const input = `"Hello \\"world\\" with quotes"
245[
246  {"type":"TEXT","name":"file.txt","content":"test"}
247]`;
248
249		const result = parseClipboardContent(input);
250
251		expect(result.message).toBe('Hello "world" with quotes');
252		expect(result.textAttachments).toHaveLength(1);
253	});
254
255	it('handles newlines in message', () => {
256		const input = `"Hello\\nworld"
257[
258  {"type":"TEXT","name":"file.txt","content":"test"}
259]`;
260
261		const result = parseClipboardContent(input);
262
263		expect(result.message).toBe('Hello\nworld');
264		expect(result.textAttachments).toHaveLength(1);
265	});
266
267	it('returns message only when no array follows', () => {
268		const input = '"Just a quoted string"';
269
270		const result = parseClipboardContent(input);
271
272		expect(result.message).toBe('Just a quoted string');
273		expect(result.textAttachments).toHaveLength(0);
274	});
275
276	it('filters out invalid attachment objects', () => {
277		const input = `"Hello"
278[
279  {"type":"TEXT","name":"valid.txt","content":"valid"},
280  {"type":"INVALID","name":"invalid.txt","content":"invalid"},
281  {"name":"missing-type.txt","content":"missing"},
282  {"type":"TEXT","content":"missing name"}
283]`;
284
285		const result = parseClipboardContent(input);
286
287		expect(result.message).toBe('Hello');
288		expect(result.textAttachments).toHaveLength(1);
289		expect(result.textAttachments[0].name).toBe('valid.txt');
290	});
291
292	it('handles empty attachments array', () => {
293		const input = '"Hello"\n[]';
294
295		const result = parseClipboardContent(input);
296
297		expect(result.message).toBe('Hello');
298		expect(result.textAttachments).toHaveLength(0);
299	});
300
301	it('roundtrips correctly with formatMessageForClipboard', () => {
302		const originalContent = 'Hello "world" with\nspecial characters';
303		const originalExtras = [
304			{
305				type: AttachmentType.TEXT as const,
306				name: 'file1.txt',
307				content: 'Content with\nnewlines and "quotes"'
308			},
309			{
310				type: AttachmentType.TEXT as const,
311				name: 'file2.txt',
312				content: 'Another file'
313			}
314		];
315
316		const formatted = formatMessageForClipboard(originalContent, originalExtras);
317		const parsed = parseClipboardContent(formatted);
318
319		expect(parsed.message).toBe(originalContent);
320		expect(parsed.textAttachments).toHaveLength(2);
321		expect(parsed.textAttachments[0].name).toBe('file1.txt');
322		expect(parsed.textAttachments[0].content).toBe('Content with\nnewlines and "quotes"');
323		expect(parsed.textAttachments[1].name).toBe('file2.txt');
324		expect(parsed.textAttachments[1].content).toBe('Another file');
325	});
326});
327
328describe('hasClipboardAttachments', () => {
329	it('returns false for plain text', () => {
330		expect(hasClipboardAttachments('Hello world')).toBe(false);
331	});
332
333	it('returns false for empty string', () => {
334		expect(hasClipboardAttachments('')).toBe(false);
335	});
336
337	it('returns false for quoted string without attachments', () => {
338		expect(hasClipboardAttachments('"Hello world"')).toBe(false);
339	});
340
341	it('returns true for valid format with attachments', () => {
342		const input = `"Hello"
343[{"type":"TEXT","name":"file.txt","content":"test"}]`;
344
345		expect(hasClipboardAttachments(input)).toBe(true);
346	});
347
348	it('returns false for format with empty attachments array', () => {
349		const input = '"Hello"\n[]';
350
351		expect(hasClipboardAttachments(input)).toBe(false);
352	});
353
354	it('returns false for malformed JSON', () => {
355		expect(hasClipboardAttachments('"Hello"\n[broken')).toBe(false);
356	});
357});
358
359describe('roundtrip edge cases', () => {
360	it('preserves empty message with attachments', () => {
361		const extras = [
362			{
363				type: AttachmentType.TEXT as const,
364				name: 'file.txt',
365				content: 'Content only'
366			}
367		];
368		const formatted = formatMessageForClipboard('', extras);
369		const parsed = parseClipboardContent(formatted);
370
371		expect(parsed.message).toBe('');
372		expect(parsed.textAttachments).toHaveLength(1);
373		expect(parsed.textAttachments[0].content).toBe('Content only');
374	});
375
376	it('preserves attachment with empty content', () => {
377		const extras = [
378			{
379				type: AttachmentType.TEXT as const,
380				name: 'empty.txt',
381				content: ''
382			}
383		];
384		const formatted = formatMessageForClipboard('Message', extras);
385		const parsed = parseClipboardContent(formatted);
386
387		expect(parsed.message).toBe('Message');
388		expect(parsed.textAttachments).toHaveLength(1);
389		expect(parsed.textAttachments[0].content).toBe('');
390	});
391
392	it('preserves multiple backslashes', () => {
393		const content = 'Path: C:\\\\Users\\\\test\\\\file.txt';
394		const extras = [
395			{
396				type: AttachmentType.TEXT as const,
397				name: 'path.txt',
398				content: 'D:\\\\Data\\\\file'
399			}
400		];
401		const formatted = formatMessageForClipboard(content, extras);
402		const parsed = parseClipboardContent(formatted);
403
404		expect(parsed.message).toBe(content);
405		expect(parsed.textAttachments[0].content).toBe('D:\\\\Data\\\\file');
406	});
407
408	it('preserves tabs and various whitespace', () => {
409		const content = 'Line1\t\tTabbed\n  Spaced\r\nCRLF';
410		const extras = [
411			{
412				type: AttachmentType.TEXT as const,
413				name: 'whitespace.txt',
414				content: '\t\t\n\n   '
415			}
416		];
417		const formatted = formatMessageForClipboard(content, extras);
418		const parsed = parseClipboardContent(formatted);
419
420		expect(parsed.message).toBe(content);
421		expect(parsed.textAttachments[0].content).toBe('\t\t\n\n   ');
422	});
423});