import { describe, expect, it } from 'vitest';
import { latexSafeAiJsonSource, parseRecoverableAiJson, parseRecoverableAiJsonObject } from '../ai-structured-output';

describe('AI structured-output recovery', () => {
  it('preserves valid JSON and strips markdown fences/protocol markers', () => {
    expect(parseRecoverableAiJson('```json\n[LEARNITIVE_JSON]\n{"replacement":"clear text","confidence":0.9}\n```')).toEqual({ replacement: 'clear text', confidence: 0.9 });
  });

  it('protects unescaped LaTeX commands that collide with JSON escapes', () => {
    const parsed = parseRecoverableAiJson('{"replacement":"Use \\textbf{strong}, \\ref{sec:intro} and \\cite{smith}."}') as { replacement: string };
    expect(parsed.replacement).toBe('Use \\textbf{strong}, \\ref{sec:intro} and \\cite{smith}.');
    expect(parsed.replacement).not.toContain('\t');
    expect(parsed.replacement).not.toContain('\r');
  });

  it('closes a structurally truncated object without inventing semantic string content', () => {
    expect(parseRecoverableAiJson('{"replacement":"A complete value","meta":{"score":0.8', { recoverTruncation: true })).toEqual({ replacement: 'A complete value', meta: { score: 0.8 } });
  });

  it('supports reviewer arrays when explicitly allowed', () => {
    expect(parseRecoverableAiJson('[{"title":"One"},{"title":"Two"}]', { allowArray: true })).toEqual([{ title: 'One' }, { title: 'Two' }]);
  });

  it('fails closed when there is no structured JSON rather than treating prose as an object', () => {
    expect(parseRecoverableAiJsonObject('Replace the sentence with clearer prose.')).toBeUndefined();
  });

  it('fails closed on mismatched structural delimiters', () => {
    expect(() => parseRecoverableAiJson('{"findings":[1,2}}', { allowArray: true })).toThrow(/Mismatched JSON delimiters/u);
  });

  it('protects uncommon and custom TeX control words without an allow-list', () => {
    const parsed = parseRecoverableAiJson('{"replacement":"\\normalfont \\numberwithin{equation}{section} \\newpage \\notacommonmacro"}') as { replacement: string };
    expect(parsed.replacement).toBe('\\normalfont \\numberwithin{equation}{section} \\newpage \\notacommonmacro');
  });

  it('keeps an ordinary JSON newline escape before an uppercase prose boundary', () => {
    const parsed = parseRecoverableAiJson('{"text":"First\\nHeading"}') as { text: string };
    expect(parsed.text).toBe('First\nHeading');
  });

  it('protects one-letter braced TeX controls that collide with JSON escapes', () => {
    const parsed = parseRecoverableAiJson('{"replacement":"\\n{argument}"}') as { replacement: string };
    expect(parsed.replacement).toBe('\\n{argument}');
  });

  it('encodes raw control characters only inside model-authored JSON strings', () => {
    const safe = latexSafeAiJsonSource('{"text":"line one\nline two\tvalue"}');
    expect(JSON.parse(safe)).toEqual({ text: 'line one\nline two\tvalue' });
  });
});
