import { WORKSPACE_STORAGE_TYPE } from '@learnitive/constants';
import { projectFilePath } from '@learnitive/project-storage';
import { createTestEnvironment } from '@learnitive/test-utils';
import { afterEach, describe, expect, it } from 'vitest';

const encoder = new TextEncoder();
const decoder = new TextDecoder();

describe('ProjectToolsService', () => {
  const controllers: AbortController[] = [];
  afterEach(() => {
    for (const controller of controllers.splice(0)) controller.abort();
  });

  async function setup() {
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({ controller });
    const services = env.instantiateAll();
    await env.mountAll();
    await services.workspaceOps.createWorkspaceInfo({
      name: 'paper',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });
    const storage = await services.projectStorage.getProvider('paper');
    await storage.writeFile(
      projectFilePath('main.tex'),
      encoder.encode(String.raw`\documentclass{article}
\begin{document}
\input{chapters/results}
See \ref{fig:old}. Cite \cite{smith2026}.
\includegraphics{figures/result}
\bibliography{refs}
\end{document}`),
    );
    await storage.writeFile(
      projectFilePath('chapters/results.tex'),
      encoder.encode(String.raw`\section{Results}
\label{fig:old}
Results.`),
    );
    await storage.writeFile(
      projectFilePath('refs.bib'),
      encoder.encode('@article{smith2026,title={Paper}}'),
    );
    await storage.writeFile(
      projectFilePath('figures/result.png'),
      new Uint8Array([1, 2, 3]),
    );
    return { services, storage };
  }

  it('renames labels and citation keys across files without reserializing unrelated text', async () => {
    const { services, storage } = await setup();
    await services.projectTools.renameLabel('paper', 'fig:old', 'fig:new');
    await services.projectTools.renameCitationKey('paper', 'smith2026', 'smith2026a');

    expect(decoder.decode(await storage.readFile(projectFilePath('main.tex')))).toContain(
      String.raw`\ref{fig:new}`,
    );
    expect(decoder.decode(await storage.readFile(projectFilePath('chapters/results.tex')))).toContain(
      String.raw`\label{fig:new}`,
    );
    expect(decoder.decode(await storage.readFile(projectFilePath('main.tex')))).toContain(
      String.raw`\cite{smith2026a}`,
    );
    expect(decoder.decode(await storage.readFile(projectFilePath('refs.bib')))).toBe(
      '@article{smith2026a,title={Paper}}',
    );
  });

  it('moves an image and source file while updating indexed references', async () => {
    const { services, storage } = await setup();
    await services.projectTools.moveProjectFile(
      'paper',
      projectFilePath('figures/result.png'),
      projectFilePath('figures/final.png'),
    );
    await services.projectTools.moveProjectFile(
      'paper',
      projectFilePath('chapters/results.tex'),
      projectFilePath('sections/results.tex'),
    );

    const main = decoder.decode(await storage.readFile(projectFilePath('main.tex')));
    expect(main).toContain(String.raw`\includegraphics{figures/final}`);
    expect(main).toContain(String.raw`\input{sections/results}`);
    await expect(storage.readFile(projectFilePath('figures/result.png'))).resolves.toBeUndefined();
    await expect(storage.readFile(projectFilePath('sections/results.tex'))).resolves.toBeDefined();
  });

  it('updates project metadata when the configured root file is renamed', async () => {
    const { services, storage } = await setup();
    await services.projectTools.moveProjectFile(
      'paper',
      projectFilePath('main.tex'),
      projectFilePath('src/main.tex'),
    );

    expect((await services.projectStorage.getProjectConfig('paper')).rootFile).toBe('src/main.tex');
    await expect(storage.readFile(projectFilePath('main.tex'))).resolves.toBeUndefined();
    const movedRoot = decoder.decode((await storage.readFile(projectFilePath('src/main.tex')))!);
    expect(movedRoot).toContain(String.raw`\input{../chapters/results}`);
    expect(movedRoot).toContain(String.raw`\includegraphics{../figures/result}`);
    expect(movedRoot).toContain(String.raw`\bibliography{../refs}`);
  });

  it('reports semantic project health and search results', async () => {
    const { services } = await setup();
    const search = await services.projectTools.search('paper', 'Results');
    expect(search.some((item) => item.kind === 'outline')).toBe(true);
    const report = await services.projectTools.doctor('paper');
    expect(report.errors).toBe(0);
    expect(report.issues.some((issue) => issue.id.includes('unused-asset'))).toBe(false);
    expect(report.score).toBeGreaterThan(80);
  });

  it('searches nested project text with globs and performs regex replace through recovery checkpoints', async () => {
    const { services, storage } = await setup();
    const results = await services.projectTools.searchText('paper', {
      query: 'Results',
      includeGlobs: ['*.tex'],
      caseSensitive: true,
    });

    expect(results.some((item) => item.file === 'chapters/results.tex')).toBe(true);
    expect(results.every((item) => item.anchor.filePath === item.file)).toBe(true);

    const nested = results.filter((item) => item.file === 'chapters/results.tex');
    const replacement = await services.projectTools.replaceText(
      'paper',
      {
        query: '(Results)',
        regex: true,
        caseSensitive: true,
        scope: { kind: 'file', file: projectFilePath('chapters/results.tex') },
      },
      'Reviewed $1',
      [nested[0]!.id],
    );

    expect(replacement.replaced).toBe(1);
    const source = decoder.decode(await storage.readFile(projectFilePath('chapters/results.tex')));
    expect(source).toContain('Reviewed Results');
    expect(source).toContain('Results.');

    const checkpoints = await services.noteSnapshot.listSnapshots({ wsName: 'paper' });
    const checkpoint = checkpoints.find((item) => item.wsPath === 'paper:chapters/results.tex');
    expect(checkpoint).toBeDefined();
    expect((await services.noteSnapshot.getSnapshot(checkpoint!.id))?.content).toContain('\\section{Results}');
  });
});
