import { describe, expect, it } from 'vitest';
import { parseSyncTexText } from '../synctex-parser';

const sp = (points: number) => Math.round(points * 65536);
const SAMPLE = [
  'SyncTeX Version:1',
  'Input:1:/work/main.tex',
  'Input:2:/work/chapters/results.tex',
  'Content:',
  '{1',
  `h1,3:${sp(20)},${sp(40)}:${sp(100)},${sp(10)},${sp(2)}`,
  `h2,42:${sp(30)},${sp(60)}:${sp(200)},${sp(12)},${sp(3)}`,
  '}',
  '{2',
  `h2,80:${sp(25)},${sp(50)}:${sp(180)},${sp(11)},${sp(2)}`,
  '}',
  'Postamble:',
].join('\n');

describe('parseSyncTexText', () => {
  it('maps a nested source file to the correct PDF page and rectangle', () => {
    const index = parseSyncTexText(SAMPLE);
    const mapped = index.forward('chapters/results.tex', 42);
    expect(mapped?.page).toBe(1);
    expect(mapped?.line).toBe(42);
    expect(mapped?.rects[0]).toMatchObject({ x: 30, y: 48, width: 200, height: 15 });
    expect(index.pagesForFile('chapters/results.tex')).toEqual([1, 2]);
  });

  it('reverse maps PDF coordinates to the correct source file and line', () => {
    const index = parseSyncTexText(SAMPLE);
    expect(index.reverse(2, 40, 45)).toMatchObject({
      file: 'work/chapters/results.tex',
      line: 80,
    });
  });

  it('prefers a concrete line box over an overlapping container box', () => {
    const overlap = [
      'SyncTeX Version:1',
      'Input:1:/work/main.tex',
      'Content:',
      '{1',
      `[1,20:${sp(20)},${sp(40)}:${sp(100)},${sp(10)},${sp(2)}`,
      `h1,20:${sp(20)},${sp(40)}:${sp(100)},${sp(10)},${sp(2)}`,
      '}',
      'Postamble:',
    ].join('\n');
    const index = parseSyncTexText(overlap);
    expect(index.reverse(1, 30, 35)).toMatchObject({
      file: 'work/main.tex',
      line: 20,
    });
  });

  it('uses basename fallback only when no stronger path match exists', () => {
    const index = parseSyncTexText(SAMPLE);
    expect(index.forward('results.tex', 80)?.page).toBe(2);
    expect(index.forward('chapters/results.tex', 80)?.page).toBe(2);
  });
});

it('BF2 applies SyncTeX unit, magnification and origin offsets consistently', () => {
  const scaled = [
    'SyncTeX Version:1',
    'Input:1:/work/main.tex',
    'Magnification:2000',
    'Unit:2',
    `X Offset:${sp(5)}`,
    `Y Offset:${sp(7)}`,
    'Content:',
    '{1',
    `h1,10:${sp(10)},${sp(20)}:${sp(30)},${sp(4)},${sp(1)}`,
    '}',
    'Postamble:',
  ].join('\n');
  const mapped = parseSyncTexText(scaled).forward('main.tex', 10);
  expect(mapped?.rects[0]).toMatchObject({
    x: 50,
    y: 78,
    width: 120,
    height: 20,
  });
});

it('BF2 uses column evidence when a line has multiple SyncTeX boxes', () => {
  const columns = [
    'SyncTeX Version:1',
    'Input:1:/work/main.tex',
    'Content:',
    '{1',
    `h1,20,3:${sp(10)},${sp(30)}:${sp(20)},${sp(5)},${sp(1)}`,
    `h1,20,40:${sp(80)},${sp(30)}:${sp(20)},${sp(5)},${sp(1)}`,
    '}',
    'Postamble:',
  ].join('\n');
  expect(parseSyncTexText(columns).forward('main.tex', 20, 39)?.rects[0]?.x).toBe(80);
});

it('BF2 bounds nearest-line fallback instead of jumping to unrelated source', () => {
  expect(parseSyncTexText(SAMPLE).forward('main.tex', 100)).toBeNull();
});

it('BF2 fails closed for ambiguous basename-only input matches', () => {
  const ambiguous = [
    'SyncTeX Version:1',
    'Input:1:/work/a/main.tex',
    'Input:2:/work/b/main.tex',
    'Content:',
    '{1',
    `h1,10:${sp(10)},${sp(20)}:${sp(30)},${sp(4)},${sp(1)}`,
    `h2,10:${sp(60)},${sp(20)}:${sp(30)},${sp(4)},${sp(1)}`,
    '}',
    'Postamble:',
  ].join('\n');
  const index = parseSyncTexText(ambiguous);
  expect(index.forward('main.tex', 10)).toBeNull();
  expect(index.pagesForFile('main.tex')).toEqual([]);
});

it('BF2 reverse mapping returns the recorded container line without an ad-hoc minus-one', () => {
  const onlyContainer = [
    'SyncTeX Version:1',
    'Input:1:/work/main.tex',
    'Content:',
    '{1',
    `[1,33:${sp(10)},${sp(30)}:${sp(80)},${sp(8)},${sp(2)}`,
    '}',
    'Postamble:',
  ].join('\n');
  expect(parseSyncTexText(onlyContainer).reverse(1, 20, 25)?.line).toBe(33);
});


it('BF2 applies post-scriptum magnification and dimensioned origin overrides like the reference scanner', () => {
  const post = [
    'SyncTeX Version:1',
    'Input:1:/work/main.tex',
    'Magnification:1000',
    'Unit:1',
    'X Offset:0',
    'Y Offset:0',
    'Content:',
    '{1',
    `h1,10:${sp(10)},${sp(20)}:${sp(30)},${sp(4)},${sp(1)}`,
    '}',
    'Postamble:',
    'Count:1',
    'Post scriptum:',
    'Magnification:2',
    'X Offset:1pt',
    'Y Offset:2pt',
  ].join('\n');
  const mapped = parseSyncTexText(post).forward('main.tex', 10);
  expect(mapped?.rects[0]?.x).toBeCloseTo(21, 6);
  expect(mapped?.rects[0]?.y).toBeCloseTo(34, 6);
  expect(mapped?.rects[0]?.width).toBeCloseTo(60, 6);
  expect(mapped?.rects[0]?.height).toBeCloseTo(10, 6);
});

it('BF2 reverse hit testing handles valid negative-width SyncTeX boxes', () => {
  const negative = [
    'SyncTeX Version:1',
    'Input:1:/work/main.tex',
    'Content:',
    '{1',
    `h1,55:${sp(80)},${sp(40)}:${-sp(30)},${sp(5)},${sp(1)}`,
    '}',
    'Postamble:',
  ].join('\n');
  expect(parseSyncTexText(negative).reverse(1, 60, 37)).toMatchObject({
    file: 'work/main.tex',
    line: 55,
  });
});
