import { describe, expect, it } from 'vitest';
import { projectFilePath } from '@learnitive/project-storage';
import { parseLatexFile } from '@learnitive/project-semantic-index';
import {
  createLatexVisualDocument,
  deriveVisualSourceEdits,
  visualMoveRangeForBlock,
} from '../index';

describe('LaTeX visual projection', () => {
  it('R14.9 isolates an unsupported inline command without freezing its paragraph', () => {
    const source = '\\begin{document}\n\\section{Title}\nBefore \\foo{bar} after.\n\\end{document}';
    const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
    expect(blocks.map((block) => block.kind)).toEqual(['heading', 'rich-paragraph']);
    const doc = createLatexVisualDocument(blocks);
    const paragraph = doc.child(1);
    expect(paragraph.type.name).toBe('visual_rich_paragraph');
    const raw = Array.from({ length: paragraph.childCount }, (_, index) => paragraph.child(index)).find(
      (node) => node.attrs.visualKind === 'raw',
    );
    expect(raw?.textContent).toBe('\\foo{bar}');
  });

  it('creates a targeted edit for changed heading text only', () => {
    const source = '\\begin{document}\n\\section{Old}\nText\n\\end{document}';
    const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
    const before = createLatexVisualDocument(blocks);
    const heading = before.child(0);
    const changed = heading.type.create(heading.attrs, before.type.schema.text('New'));
    const after = before.copy(before.content.replaceChild(0, changed));
    expect(deriveVisualSourceEdits(before, after)).toEqual([
      { from: source.indexOf('Old'), to: source.indexOf('Old') + 3, text: 'New' },
    ]);
  });
});

it('keeps citation atoms structural while allowing surrounding rich paragraph text edits', () => {
  const source = '\\begin{document}\nBefore \\cite{smith2026} after.\n\\end{document}';
  const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
  const before = createLatexVisualDocument(blocks);
  const rich = before.child(0);
  expect(rich.type.name).toBe('visual_rich_paragraph');
  const firstText = rich.child(0);
  const changedFirstText = firstText.type.create(firstText.attrs, before.type.schema.text('Earlier '));
  const changedRich = rich.copy(rich.content.replaceChild(0, changedFirstText));
  const after = before.copy(before.content.replaceChild(0, changedRich));
  expect(deriveVisualSourceEdits(before, after)).toEqual([
    {
      from: source.indexOf('Before'),
      to: source.indexOf('Before') + 'Before '.length,
      text: 'Earlier ',
    },
  ]);
});

it('R14.13.1 keeps visual lists schema-generatable without synthesizing unmapped items', () => {
  const source = String.raw`\begin{document}
\begin{itemize}
\item First
\item Second
\end{itemize}
\end{document}`;
  const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
  const doc = createLatexVisualDocument(blocks);
  const list = doc.child(0);
  expect(list.type.name).toBe('visual_list');
  expect(list.childCount).toBe(2);
  expect(list.child(0).type.name).toBe('visual_list_item');
});

it('keeps table rows schema-generatable without synthesizing unmapped cells', () => {
  const source = String.raw`\begin{document}
\begin{tabular}{cc}
A & B \\
\end{tabular}
\end{document}`;
  const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
  const doc = createLatexVisualDocument(blocks);
  const table = doc.child(0);
  expect(table.type.name).toBe('visual_table');
  expect(table.childCount).toBeGreaterThan(0);
  expect(table.child(0).type.name).toBe('visual_table_row');
  expect(table.child(0).childCount).toBeGreaterThan(0);
  expect(table.child(0).child(0).type.name).toBe('visual_table_cell');
});

it('creates targeted edits for safe table cells', () => {
  const source = String.raw`\begin{document}
\begin{table}
\begin{tabular}{cc}
A & B \\
1 & 2 \\
\end{tabular}
\end{table}
\end{document}`;
  const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
  const before = createLatexVisualDocument(blocks);
  const table = before.child(0);
  expect(table.type.name).toBe('visual_table');
  const firstRow = table.child(0);
  const firstCell = firstRow.child(0);
  const firstInline = firstCell.child(0);
  const changedInline = firstInline.type.create(
    firstInline.attrs,
    before.type.schema.text('Alpha'),
  );
  const changedCell = firstCell.type.create(firstCell.attrs, changedInline);
  const changedRow = firstRow.copy(firstRow.content.replaceChild(0, changedCell));
  const changedTable = table.copy(table.content.replaceChild(0, changedRow));
  const after = before.copy(before.content.replaceChild(0, changedTable));
  expect(deriveVisualSourceEdits(before, after)).toEqual([
    { from: source.indexOf('A &'), to: source.indexOf('A &') + 1, text: 'Alpha' },
  ]);
});

it('R1 keeps canonical hard breaks editable inside simple table cells', () => {
  const source = String.raw`\begin{document}
\begin{tabular}{cc}
A\newline{}B & C \\
\end{tabular}
\end{document}`;
  const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
  const doc = createLatexVisualDocument(blocks);
  const table = doc.child(0);
  expect(table.type.name).toBe('visual_table');
  const cell = table.child(0).child(0);
  expect(cell.type.name).toBe('visual_table_cell');
  expect(cell.childCount).toBe(3);
  expect(cell.child(0).type.name).toBe('visual_source_text');
  expect(cell.child(1).type.name).toBe('visual_hard_break');
  expect(cell.child(2).type.name).toBe('visual_source_text');
});

it('R14.4 projects source-backed equations as first-class visual math nodes', () => {
  const source = String.raw`\begin{document}
Inline $E=mc^2$ and display:
\[
\frac{a}{b}
\]
\end{document}`;
  const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
  const doc = createLatexVisualDocument(blocks);
  const mathNodes: string[] = [];
  doc.descendants((node) => {
    if (node.type.name === 'visual_math') mathNodes.push(String(node.attrs.mathKind));
    return true;
  });
  expect(mathNodes).toEqual(['inline', 'display']);
});

it('R14.9 derives edits only for safe formatting bodies while preserving wrappers', () => {
  const source = String.raw`\begin{document}
Before \custommacro{LOCKED} after \textbf{important}.
\end{document}`;
  const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
  const before = createLatexVisualDocument(blocks);
  const paragraph = before.child(0);
  expect(paragraph.type.name).toBe('visual_rich_paragraph');
  const boldIndex = Array.from({ length: paragraph.childCount }, (_, index) => index).find(
    (index) => paragraph.child(index).attrs.visualKind === 'bold',
  );
  expect(boldIndex).not.toBeUndefined();
  const bold = paragraph.child(boldIndex!);
  const changedBold = bold.type.create(bold.attrs, before.type.schema.text('critical'));
  const changedParagraph = paragraph.copy(paragraph.content.replaceChild(boldIndex!, changedBold));
  const after = before.copy(before.content.replaceChild(0, changedParagraph));
  expect(deriveVisualSourceEdits(before, after)).toEqual([
    {
      from: source.indexOf('important'),
      to: source.indexOf('important') + 'important'.length,
      text: 'critical',
    },
  ]);
});

it('R14.9 creates editable list items without exposing item markers as editable source', () => {
  const source = String.raw`\begin{document}
\begin{itemize}
\item First item
\item Second item
\end{itemize}
\end{document}`;
  const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
  const before = createLatexVisualDocument(blocks);
  const list = before.child(0);
  expect(list.type.name).toBe('visual_list');
  const item = list.child(0);
  const text = item.child(0);
  const changedText = text.type.create(text.attrs, before.type.schema.text('Updated item'));
  const changedItem = item.copy(item.content.replaceChild(0, changedText));
  const changedList = list.copy(list.content.replaceChild(0, changedItem));
  const after = before.copy(before.content.replaceChild(0, changedList));
  expect(deriveVisualSourceEdits(before, after)).toEqual([
    {
      from: source.indexOf('First item'),
      to: source.indexOf('First item') + 'First item'.length,
      text: 'Updated item',
    },
  ]);
});

it('R14.12 moves a section with its complete semantic subtree', () => {
  const source = String.raw`\begin{document}
\section{One}
One paragraph.
\subsection{One A}
Nested paragraph.
\section{Two}
Two paragraph.
\end{document}`;
  const blocks = parseLatexFile(
    projectFilePath('main.tex'),
    source,
  ).visualBlocks;
  const sectionOne = blocks.find(
    (block) => block.kind === 'heading' && block.text === 'One',
  );
  const sectionTwo = blocks.find(
    (block) => block.kind === 'heading' && block.text === 'Two',
  );
  if (!sectionOne || !sectionTwo) throw new Error('sections missing');

  const range = visualMoveRangeForBlock(blocks, sectionOne.id);
  expect(range).toBeDefined();
  const moved = source.slice(range!.from, range!.to);
  expect(moved).toContain('\\section{One}');
  expect(moved).toContain('One paragraph.');
  expect(moved).toContain('\\subsection{One A}');
  expect(moved).toContain('Nested paragraph.');
  expect(moved).not.toContain('\\section{Two}');
  expect(range!.to).toBe(sectionTwo.range.from);
});

it('R14.12 moves a subsection only through the next same-or-higher heading', () => {
  const source = String.raw`\begin{document}
\section{One}
\subsection{A}
A body.
\subsubsection{Deep}
Deep body.
\subsection{B}
B body.
\section{Two}
Two body.
\end{document}`;
  const blocks = parseLatexFile(
    projectFilePath('main.tex'),
    source,
  ).visualBlocks;
  const subsectionA = blocks.find(
    (block) => block.kind === 'heading' && block.text === 'A',
  );
  const subsectionB = blocks.find(
    (block) => block.kind === 'heading' && block.text === 'B',
  );
  if (!subsectionA || !subsectionB) throw new Error('subsections missing');

  const range = visualMoveRangeForBlock(blocks, subsectionA.id);
  expect(range).toBeDefined();
  const moved = source.slice(range!.from, range!.to);
  expect(moved).toContain('\\subsection{A}');
  expect(moved).toContain('A body.');
  expect(moved).toContain('\\subsubsection{Deep}');
  expect(moved).toContain('Deep body.');
  expect(moved).not.toContain('\\subsection{B}');
  expect(range!.to).toBe(subsectionB.range.from);
});

it('BF2 materializes an empty list item as a source-backed editable text host', () => {
  const source = String.raw`\begin{document}
\begin{itemize}
\item First
\item 
\end{itemize}
\end{document}`;
  const blocks = parseLatexFile(projectFilePath('main.tex'), source).visualBlocks;
  const before = createLatexVisualDocument(blocks);
  const list = before.child(0);
  const emptyItem = list.child(1);
  expect(emptyItem.type.name).toBe('visual_list_item');
  expect(emptyItem.childCount).toBe(1);
  const emptyText = emptyItem.child(0);
  expect(emptyText.type.name).toBe('visual_source_text');
  expect(emptyText.attrs.bodyFrom).toBe(emptyText.attrs.bodyTo);

  const typedText = emptyText.type.create(emptyText.attrs, before.type.schema.text('Second'));
  const changedItem = emptyItem.copy(emptyItem.content.replaceChild(0, typedText));
  const changedList = list.copy(list.content.replaceChild(1, changedItem));
  const after = before.copy(before.content.replaceChild(0, changedList));
  expect(deriveVisualSourceEdits(before, after)).toEqual([
    {
      from: Number(emptyText.attrs.bodyFrom),
      to: Number(emptyText.attrs.bodyTo),
      text: 'Second',
    },
  ]);
});
