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

const WS_NAME = 'latex-project';
const MAIN_TEX = `${WS_NAME}:main.tex`;

async function setup() {
  const env = createTestEnvironment();
  const services = env.instantiateAll();
  await env.mountAll();
  await services.workspaceOps.createWorkspaceInfo({
    name: WS_NAME,
    type: WORKSPACE_STORAGE_TYPE.Memory,
    metadata: {},
  });
  await services.fileSystem.createTextFile(
    MAIN_TEX,
    '\\documentclass{article}\n\\begin{document}\nOriginal\n\\end{document}\n',
  );
  await services.latexSource.load(MAIN_TEX);
  return { env, services };
}

describe('LatexSourceService', () => {
  it('keeps source edits dirty until their durable write finishes', async () => {
    const { env, services } = await setup();
    const edited = '\\documentclass{article}\nEdited\n';

    services.latexSource.update(MAIN_TEX, edited, 'source');
    expect(services.latexSource.hasPendingOrFailedSave(MAIN_TEX)).toBe(true);

    await services.latexSource.flush(MAIN_TEX);
    expect(services.latexSource.hasPendingOrFailedSave(MAIN_TEX)).toBe(false);
    expect(await services.fileSystem.readFileAsText(MAIN_TEX)).toBe(edited);
    env.controller.abort();
  });

  it('flushes only source documents in the requested workspace', async () => {
    const { env, services } = await setup();
    const otherWorkspace = 'other-latex-project';
    const otherMainTex = `${otherWorkspace}:main.tex`;
    await services.workspaceOps.createWorkspaceInfo({
      name: otherWorkspace,
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });
    await services.fileSystem.createTextFile(otherMainTex, 'Other original\n');
    await services.latexSource.load(otherMainTex);

    vi.useFakeTimers();
    try {
      services.latexSource.update(MAIN_TEX, 'Primary changed\n', 'source');
      services.latexSource.update(otherMainTex, 'Other changed\n', 'source');
      await services.latexSource.flushWorkspace(WS_NAME);

      expect(await services.fileSystem.readFileAsText(MAIN_TEX)).toBe(
        'Primary changed\n',
      );
      expect(await services.fileSystem.readFileAsText(otherMainTex)).toBe(
        'Other original\n',
      );
      expect(
        services.latexSource.hasPendingOrFailedSaveInWorkspace(otherWorkspace),
      ).toBe(true);
    } finally {
      vi.clearAllTimers();
      vi.useRealTimers();
      env.controller.abort();
    }
  });

  it('supports service-owned undo and redo', async () => {
    const { env, services } = await setup();
    const original = services.latexSource.getSnapshot(MAIN_TEX)?.text ?? '';

    services.latexSource.update(MAIN_TEX, `${original}% edit\n`, 'source');
    expect(services.latexSource.undo(MAIN_TEX)).toBe(true);
    expect(services.latexSource.getSnapshot(MAIN_TEX)?.text).toBe(original);
    expect(services.latexSource.redo(MAIN_TEX)).toBe(true);
    expect(services.latexSource.getSnapshot(MAIN_TEX)?.text).toContain('% edit');
    env.controller.abort();
  });

  it('preserves the previous TeX source in local recovery snapshots', async () => {
    const { env, services } = await setup();
    const original = services.latexSource.getSnapshot(MAIN_TEX)?.text ?? '';

    services.latexSource.update(MAIN_TEX, `${original}% changed\n`, 'source');
    await services.latexSource.flush(MAIN_TEX);

    const snapshots = await services.noteSnapshot.listSnapshots({ wsName: WS_NAME });
    expect(snapshots).toHaveLength(1);
    expect(snapshots[0]?.wsPath).toBe(MAIN_TEX);
    expect(
      (await services.noteSnapshot.getSnapshot(snapshots[0]?.id ?? ''))?.content,
    ).toBe(original);
    env.controller.abort();
  });

  it('applies targeted edits without replacing unrelated source', async () => {
    const { env, services } = await setup();
    const source = services.latexSource.getSnapshot(MAIN_TEX)?.text ?? '';
    const index = source.indexOf('Original');

    services.latexSource.applyEdit(
      MAIN_TEX,
      { from: index, to: index + 'Original'.length, text: 'Result' },
      'refactor',
    );

    const result = services.latexSource.getSnapshot(MAIN_TEX)?.text ?? '';
    expect(result).toContain('\\documentclass{article}');
    expect(result).toContain('Result');
    expect(result).not.toContain('Original');
    env.controller.abort();
  });

  it('R14.16 distinguishes passive reveal from explicit source selection', async () => {
    const { env, services } = await setup();

    const passive = services.latexSource.requestReveal(MAIN_TEX, 12, 7);
    expect(passive).toMatchObject({ line: 12, column: 7, select: false });
    expect(services.latexSource.getRevealRequest(MAIN_TEX)).toEqual(passive);

    const explicit = services.latexSource.requestSelect(MAIN_TEX, 3, 2);
    expect(explicit).toMatchObject({ line: 3, column: 2, select: true });
    expect(explicit.requestId).toBeGreaterThan(passive.requestId);
    expect(services.latexSource.getRevealRequest(MAIN_TEX)).toEqual(explicit);
    env.controller.abort();
  });

  it('R14.16 applies CodeMirror-style multi-range edits against one canonical revision', async () => {
    const { env, services } = await setup();
    const original = services.latexSource.getSnapshot(MAIN_TEX)?.text ?? '';
    const first = original.indexOf('article');
    const second = original.indexOf('Original');

    services.latexSource.applyEdits(
      MAIN_TEX,
      [
        { from: first, to: first + 'article'.length, text: 'report' },
        { from: second, to: second + 'Original'.length, text: 'Edited' },
      ],
      'source',
    );

    expect(services.latexSource.getSnapshot(MAIN_TEX)?.text).toContain(
      '\\documentclass{report}',
    );
    expect(services.latexSource.getSnapshot(MAIN_TEX)?.text).toContain('Edited');
    env.controller.abort();
  });
});

it('R14.12 groups consecutive Visual typing into one canonical undo step', async () => {
  const { env, services } = await setup();
  const original = services.latexSource.getSnapshot(MAIN_TEX)?.text ?? '';

  services.latexSource.update(MAIN_TEX, `${original}% v1\n`, 'visual');
  services.latexSource.update(MAIN_TEX, `${original}% v12\n`, 'visual');
  services.latexSource.update(MAIN_TEX, `${original}% v123\n`, 'visual');

  expect(services.latexSource.undo(MAIN_TEX)).toBe(true);
  expect(services.latexSource.getSnapshot(MAIN_TEX)?.text).toBe(original);
  expect(services.latexSource.redo(MAIN_TEX)).toBe(true);
  expect(services.latexSource.getSnapshot(MAIN_TEX)?.text).toBe(
    `${original}% v123\n`,
  );
  env.controller.abort();
});

it('R14.12 keeps Visual structural edits as separate undo transactions', async () => {
  const { env, services } = await setup();
  const original = services.latexSource.getSnapshot(MAIN_TEX)?.text ?? '';
  const typed = `${original}% typed\n`;
  const structured = `${typed}\\section{Inserted}\n`;

  services.latexSource.update(MAIN_TEX, typed, 'visual');
  services.latexSource.update(MAIN_TEX, structured, 'visual-structure');

  expect(services.latexSource.undo(MAIN_TEX)).toBe(true);
  expect(services.latexSource.getSnapshot(MAIN_TEX)?.text).toBe(typed);
  expect(services.latexSource.undo(MAIN_TEX)).toBe(true);
  expect(services.latexSource.getSnapshot(MAIN_TEX)?.text).toBe(original);
  env.controller.abort();
});

it('R14.17 leaves local Source undo to CodeMirror while preserving older canonical history', async () => {
  const { env, services } = await setup();
  const original = services.latexSource.getSnapshot(MAIN_TEX)?.text ?? '';

  services.latexSource.update(MAIN_TEX, `${original}% visual\n`, 'visual');
  expect(services.latexSource.getSnapshot(MAIN_TEX)?.canUndo).toBe(true);

  const current = services.latexSource.getSnapshot(MAIN_TEX)?.text ?? '';
  services.latexSource.applyEdits(
    MAIN_TEX,
    [{ from: current.length, to: current.length, text: '% source\n' }],
    'source',
    { history: 'ignore' },
  );

  expect(services.latexSource.getSnapshot(MAIN_TEX)?.canUndo).toBe(true);
  expect(services.latexSource.getSnapshot(MAIN_TEX)?.canRedo).toBe(false);
  expect(services.latexSource.getSnapshot(MAIN_TEX)?.text).toContain('% source');
  env.controller.abort();
  it('emits mutation events only for canonical text revisions, not save-status churn', async () => {
    const { env, services } = await setup();
    const events: Array<{ wsPath: string; wsName: string; revision: number; origin: string }> = [];
    const unsubscribe = services.latexSource.subscribeToMutations((event) => {
      events.push(event);
    });
    const original = services.latexSource.getSnapshot(MAIN_TEX)?.text ?? '';

    services.latexSource.update(MAIN_TEX, `${original}% live preview
`, 'visual');
    await services.latexSource.flush(MAIN_TEX);

    expect(events).toHaveLength(1);
    expect(events[0]).toMatchObject({
      wsPath: MAIN_TEX,
      wsName: WS_NAME,
      origin: 'visual',
    });
    expect(events[0]?.revision).toBeGreaterThan(0);

    unsubscribe();
    env.controller.abort();
  });

});
