import { WORKSPACE_STORAGE_TYPE } from '@learnitive/constants';
import type {
  LatexLiveDraftCompiler,
  LatexLiveDraftProbeRequest,
  LatexLiveDraftProbeResult,
  LatexLiveDraftRenderRequest,
  LatexLiveDraftRenderResult,
  LatexLiveInstantRenderRequest,
  LatexLiveInstantRenderResult,
} from '@learnitive/latex-compiler';
import { createTestEnvironment } from '@learnitive/test-utils';
import { afterEach, describe, expect, it } from 'vitest';


const sp = (points: number) => Math.round(points * 65_536);
const INSTANT_SYNCTEX = new TextEncoder().encode(
  [
    'SyncTeX Version:1',
    'Input:1:/work/main.tex',
    'Content:',
    '{1',
    `h1,3:${sp(72)},${sp(120)}:${sp(300)},${sp(10)},${sp(2)}`,
    `h1,4:${sp(72)},${sp(136)}:${sp(300)},${sp(10)},${sp(2)}`,
    '}',
    'Postamble:',
  ].join('\n'),
);

const SUCCESS_RESULT: LatexLiveDraftProbeResult = {
  ok: true,
  engine: 'lualatex',
  runtime: 'busytex',
  directLua: true,
  memfsRecords: true,
  pageCount: 2,
  records: [],
  fonts: [],
  hasGlyphs: false,
  hasRules: false,
  hasWhatsits: false,
  hasMultiplePages: true,
  log: 'runtime ok',
  durationMs: 10,
  certificate: {
    compileSucceeded: true,
    recordArtifactFound: true,
    recordArtifactPath: '__learnitive_live__/layout.jsonl',
    recordBytes: 96,
    directLuaRecordFound: true,
    pageRecords: 2,
    pdfBytes: 4,
    artifactReadbackAttempts: [],
  },
  pdf: new Uint8Array([37, 80, 68, 70]),
};

const RENDER_RESULT: LatexLiveDraftRenderResult = {
  ok: true,
  engine: 'lualatex',
  runtime: 'busytex',
  log: 'manuscript ok',
  durationMs: 12,
  pdf: new Uint8Array([37, 80, 68, 70, 45]),
  synctex: new TextEncoder().encode('SyncTeX Version:1\nContent:\nPostamble:'),
};

class FakeLiveDraftCompiler implements LatexLiveDraftCompiler {
  probeRequests: LatexLiveDraftProbeRequest[] = [];
  renderRequests: LatexLiveDraftRenderRequest[] = [];
  cancelCalls = 0;
  disposeCalls = 0;

  async probe(request: LatexLiveDraftProbeRequest): Promise<LatexLiveDraftProbeResult> {
    this.probeRequests.push(request);
    return SUCCESS_RESULT;
  }

  async render(request: LatexLiveDraftRenderRequest): Promise<LatexLiveDraftRenderResult> {
    this.renderRequests.push(request);
    return RENDER_RESULT;
  }

  cancel(): void {
    this.cancelCalls += 1;
  }

  dispose(): void {
    this.disposeCalls += 1;
  }
}

class InstantLiveDraftCompiler extends FakeLiveDraftCompiler {
  instantRequests: LatexLiveInstantRenderRequest[] = [];
  cancelInstantCalls = 0;

  override async render(request: LatexLiveDraftRenderRequest): Promise<LatexLiveDraftRenderResult> {
    this.renderRequests.push(request);
    return { ...RENDER_RESULT, synctex: INSTANT_SYNCTEX };
  }

  async renderInstant(request: LatexLiveInstantRenderRequest): Promise<LatexLiveInstantRenderResult> {
    this.instantRequests.push(request);
    return {
      ok: true,
      engine: 'lualatex',
      runtime: 'busytex',
      log: 'instant block ok',
      durationMs: 7,
      pdf: new Uint8Array([37, 80, 68, 70, 45, 73, 78, 83, 84]),
    };
  }

  cancelInstant(): void {
    this.cancelInstantCalls += 1;
  }
}

class FailingInstantLiveDraftCompiler extends InstantLiveDraftCompiler {
  override async renderInstant(request: LatexLiveInstantRenderRequest): Promise<LatexLiveInstantRenderResult> {
    this.instantRequests.push(request);
    return {
      ok: false,
      engine: 'lualatex',
      runtime: 'busytex',
      log: 'instant block failed',
      durationMs: 9,
      reason: 'synthetic instant failure',
    };
  }
}

class DeferredLiveDraftCompiler implements LatexLiveDraftCompiler {
  probeRequests: LatexLiveDraftProbeRequest[] = [];
  renderRequests: LatexLiveDraftRenderRequest[] = [];
  cancelCalls = 0;
  disposeCalls = 0;
  private pending: Array<{
    resolve: (result: LatexLiveDraftProbeResult) => void;
    reject: (error: unknown) => void;
    cleanup: () => void;
  }> = [];

  probe(request: LatexLiveDraftProbeRequest): Promise<LatexLiveDraftProbeResult> {
    this.probeRequests.push(request);
    return new Promise((resolve, reject) => {
      const onAbort = () => {
        cleanup();
        reject(
          request.signal?.reason ??
            new DOMException('Live PDF runtime test cancelled', 'AbortError'),
        );
      };
      const cleanup = () => request.signal?.removeEventListener('abort', onAbort);
      request.signal?.addEventListener('abort', onAbort, { once: true });
      this.pending.push({ resolve, reject, cleanup });
    });
  }

  async render(request: LatexLiveDraftRenderRequest): Promise<LatexLiveDraftRenderResult> {
    this.renderRequests.push(request);
    return RENDER_RESULT;
  }

  resolveLatest(result: LatexLiveDraftProbeResult = SUCCESS_RESULT): void {
    const pending = this.pending.at(-1);
    if (!pending) throw new Error('No pending Live Draft probe');
    pending.cleanup();
    pending.resolve(result);
  }

  cancel(): void {
    this.cancelCalls += 1;
  }

  dispose(): void {
    this.disposeCalls += 1;
  }
}

async function createWorkspace(env: ReturnType<typeof createTestEnvironment>, name: string) {
  const services = env.instantiateAll();
  await env.mountAll();
  await services.workspaceOps.createWorkspaceInfo({
    name,
    type: WORKSPACE_STORAGE_TYPE.Memory,
    metadata: {},
  });
  await services.fileSystem.createTextFile(
    `${name}:main.tex`,
    '\\documentclass{article}\n\\begin{document}\nHello\n\\end{document}\n',
  );
  await services.latexSource.load(`${name}:main.tex`);
  return services;
}

describe('ProjectLiveDraftService R4/R5 isolation', () => {
  const controllers: AbortController[] = [];

  afterEach(() => {
    for (const controller of controllers.splice(0)) controller.abort();
  });

  it('does not compile source edits until feasibility or Live PDF is explicitly requested', async () => {
    const liveCompiler = new FakeLiveDraftCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-gate');

    services.latexSource.update(
      'live-gate:main.tex',
      '\\documentclass{article}\n\\begin{document}\nChanged\n\\end{document}\n',
      'source',
    );
    await Promise.resolve();
    expect(liveCompiler.probeRequests).toHaveLength(0);
    expect(liveCompiler.renderRequests).toHaveLength(0);

    const result = await services.projectLiveDraft.probe('live-gate');
    expect(result.ok).toBe(true);
    expect(liveCompiler.probeRequests).toHaveLength(1);
    expect(liveCompiler.renderRequests).toHaveLength(0);
    expect(env.store.get(services.projectLiveDraft.$state).status).toBe('success');
    expect(env.store.get(services.projectCompile.$state).status).toBe('idle');
  });

  it('supersedes an obsolete running runtime test before starting the newer generation', async () => {
    const liveCompiler = new DeferredLiveDraftCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-supersede');

    const first = services.projectLiveDraft.probe('live-supersede');
    while (liveCompiler.probeRequests.length < 1) await Promise.resolve();
    const second = services.projectLiveDraft.probe('live-supersede');
    await expect(first).rejects.toMatchObject({ name: 'AbortError' });
    while (liveCompiler.probeRequests.length < 2) await Promise.resolve();
    liveCompiler.resolveLatest();

    await expect(second).resolves.toMatchObject({ ok: true, pageCount: 2 });
    expect(liveCompiler.cancelCalls).toBe(1);
    expect(env.store.get(services.projectCompile.$state).status).toBe('idle');
  });

  it('R5 certifies once and renders the real canonical root directly', async () => {
    const liveCompiler = new FakeLiveDraftCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-pdf-gate');

    const state = await services.projectLiveDraft.enableLivePdf('live-pdf-gate');
    expect(state).toMatchObject({ status: 'ready', enabled: true, certified: true });
    expect(state.pdf?.byteLength).toBeGreaterThan(0);
    expect(state.synctex?.byteLength).toBeGreaterThan(0);
    expect(state.rootFile).toBe('main.tex');
    expect(state.generation).toBeTypeOf('number');
    expect(liveCompiler.probeRequests).toHaveLength(1);
    expect(liveCompiler.renderRequests).toHaveLength(1);
    expect(liveCompiler.renderRequests[0]?.rootFile).toBe('main.tex');
    expect(liveCompiler.renderRequests[0]?.files.some((file) => file.path === 'main.tex')).toBe(true);
    expect(env.store.get(services.projectCompile.$state).status).toBe('idle');
  });

  it('BF4.2 carries binary figure assets into the Live PDF compiler snapshot', async () => {
    const liveCompiler = new FakeLiveDraftCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-pdf-figure-assets');
    const imageBytes = new Uint8Array([
      137, 80, 78, 71, 13, 10, 26, 10, 1, 2, 3, 4,
    ]);
    await services.fileSystem.createFile(
      'live-pdf-figure-assets:figures/result.png',
      new File([imageBytes], 'result.png', { type: 'image/png' }),
    );

    await services.projectLiveDraft.enableLivePdf('live-pdf-figure-assets');

    const render = liveCompiler.renderRequests.at(-1);
    const asset = render?.files.find((file) => file.path === 'figures/result.png');
    expect(asset).toBeDefined();
    expect(asset?.content).toEqual(imageBytes);
  });

  it('R5 reuses successful runtime certification and refreshes with render rather than probe', async () => {
    const liveCompiler = new FakeLiveDraftCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-refresh');
    await services.projectLiveDraft.probe('live-refresh');
    await services.projectLiveDraft.enableLivePdf('live-refresh');

    expect(liveCompiler.probeRequests).toHaveLength(1);
    expect(liveCompiler.renderRequests).toHaveLength(1);
    services.latexSource.update(
      'live-refresh:main.tex',
      '\\documentclass{article}\n\\begin{document}\nRefresh\n\\end{document}\n',
      'source',
    );
    await new Promise((resolve) => setTimeout(resolve, 900));
    expect(liveCompiler.probeRequests).toHaveLength(1);
    expect(liveCompiler.renderRequests.length).toBeGreaterThanOrEqual(2);
  });

  it('R6 paints an eligible source paragraph before the exact R5 debounce and then reconciles', async () => {
    const liveCompiler = new InstantLiveDraftCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-instant-r6');
    const initial = await services.projectLiveDraft.enableLivePdf('live-instant-r6');
    expect(initial).toMatchObject({ status: 'ready', backend: 'busytex' });
    expect(initial.rootFile).toBe('main.tex');
    expect(initial.generation).toBeTypeOf('number');
    expect(initial.pdf).toBeDefined();
    await services.projectPdf.acceptLivePreview(
      'live-instant-r6',
      initial.rootFile ?? 'main.tex',
      initial.generation ?? 0,
      initial.pdf ?? new Uint8Array(),
      INSTANT_SYNCTEX,
    );

    const beforeFullRenders = liveCompiler.renderRequests.length;
    const wsPath = 'live-instant-r6:main.tex';
    const source = services.latexSource.getSnapshot(wsPath);
    expect(source).toBeDefined();
    const insertAt = source?.text.indexOf('Hello') ?? -1;
    expect(insertAt).toBeGreaterThanOrEqual(0);
    services.latexSource.applyEdit(
      wsPath,
      { from: insertAt + 'Hello'.length, to: insertAt + 'Hello'.length, text: ' instantly' },
      'source',
    );

    await new Promise((resolve) => setTimeout(resolve, 140));
    expect(liveCompiler.instantRequests).toHaveLength(1);
    expect(liveCompiler.instantRequests[0]).toMatchObject({
      rootFile: 'main.tex',
      text: 'Hello instantly',
    });
    expect(liveCompiler.renderRequests).toHaveLength(beforeFullRenders);
    const instantState = env.store.get(services.projectLiveDraft.$livePdfState);
    expect(instantState.instantStatus).toBe('ready');
    expect(instantState.instantPatch).toMatchObject({
      baseGeneration: initial.generation,
      sourcePath: 'main.tex',
    });
    // R6 is transient only: the exact PDF service still points at the reconciled R5 generation.
    expect(services.projectPdf.getWorkspaceState('live-instant-r6')?.generation).toBe(initial.generation);
    expect(env.store.get(services.projectCompile.$state).status).toBe('idle');
    const diagnostics = env.store.get(services.projectLiveDraft.$instantDiagnostics);
    expect(diagnostics.mutationCount).toBeGreaterThanOrEqual(1);
    expect(diagnostics.eligibleMutationCount).toBeGreaterThanOrEqual(1);
    expect(diagnostics.renderedPatchCount).toBeGreaterThanOrEqual(1);
    expect(diagnostics.lastBlockCompileDurationMs).toBe(7);
    expect(diagnostics.lastPublishLatencyMs).toBeGreaterThanOrEqual(0);

    await new Promise((resolve) => setTimeout(resolve, 820));
    expect(liveCompiler.renderRequests.length).toBeGreaterThan(beforeFullRenders);
    const reconciled = env.store.get(services.projectLiveDraft.$livePdfState);
    expect(reconciled.status).toBe('ready');
    expect(reconciled.generation).not.toBe(initial.generation);
  });

  it('R6 fails closed for non-prose origins while exact R5 remains scheduled', async () => {
    const liveCompiler = new InstantLiveDraftCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-instant-safe-fallback');
    const initial = await services.projectLiveDraft.enableLivePdf('live-instant-safe-fallback');
    await services.projectPdf.acceptLivePreview(
      'live-instant-safe-fallback',
      initial.rootFile ?? 'main.tex',
      initial.generation ?? 0,
      initial.pdf ?? new Uint8Array(),
      INSTANT_SYNCTEX,
    );
    const beforeFullRenders = liveCompiler.renderRequests.length;
    const wsPath = 'live-instant-safe-fallback:main.tex';
    const source = services.latexSource.getSnapshot(wsPath);
    const insertAt = source?.text.indexOf('Hello') ?? -1;
    services.latexSource.applyEdit(
      wsPath,
      { from: insertAt + 'Hello'.length, to: insertAt + 'Hello'.length, text: ' reviewed' },
      'ai',
    );

    await new Promise((resolve) => setTimeout(resolve, 120));
    expect(liveCompiler.instantRequests).toHaveLength(0);
    expect(liveCompiler.renderRequests).toHaveLength(beforeFullRenders);
    const diagnostics = env.store.get(services.projectLiveDraft.$instantDiagnostics);
    expect(diagnostics.rejectedMutationCount).toBeGreaterThanOrEqual(1);
    expect(diagnostics.rejectionCounts['unsupported-origin']).toBeGreaterThanOrEqual(1);
    await new Promise((resolve) => setTimeout(resolve, 820));
    expect(liveCompiler.renderRequests.length).toBeGreaterThan(beforeFullRenders);
  });

  it('R6 records compiler failures locally and still leaves exact R5 reconciliation active', async () => {
    const liveCompiler = new FailingInstantLiveDraftCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-instant-diagnostics-failure');
    const initial = await services.projectLiveDraft.enableLivePdf('live-instant-diagnostics-failure');
    await services.projectPdf.acceptLivePreview(
      'live-instant-diagnostics-failure',
      initial.rootFile ?? 'main.tex',
      initial.generation ?? 0,
      initial.pdf ?? new Uint8Array(),
      INSTANT_SYNCTEX,
    );
    const beforeFullRenders = liveCompiler.renderRequests.length;
    const wsPath = 'live-instant-diagnostics-failure:main.tex';
    const source = services.latexSource.getSnapshot(wsPath);
    const insertAt = source?.text.indexOf('Hello') ?? -1;
    services.latexSource.applyEdit(
      wsPath,
      { from: insertAt + 'Hello'.length, to: insertAt + 'Hello'.length, text: ' safely' },
      'source',
    );

    await new Promise((resolve) => setTimeout(resolve, 140));
    expect(liveCompiler.instantRequests).toHaveLength(1);
    const diagnostics = env.store.get(services.projectLiveDraft.$instantDiagnostics);
    expect(diagnostics.eligibleMutationCount).toBeGreaterThanOrEqual(1);
    expect(diagnostics.workerFailureCount).toBeGreaterThanOrEqual(1);
    expect(diagnostics.rejectionCounts['compiler-failure']).toBeGreaterThanOrEqual(1);
    expect(diagnostics.lastBlockCompileDurationMs).toBe(9);
    expect(env.store.get(services.projectLiveDraft.$livePdfState).instantPatch).toBeUndefined();
    expect(liveCompiler.renderRequests).toHaveLength(beforeFullRenders);

    await new Promise((resolve) => setTimeout(resolve, 820));
    expect(liveCompiler.renderRequests.length).toBeGreaterThan(beforeFullRenders);
  });

  it('uses remembered Docker TeX Live Full directly for Live PDF without probing BusyTeX', async () => {
    const liveCompiler = new FakeLiveDraftCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-cloud-sticky');
    await services.projectStorage.updateProjectConfig('live-cloud-sticky', (current) => ({
      ...current,
      cloudFallbackSticky: true,
    }));

    const cloudRequests: Array<{ fallback?: boolean; engine: string }> = [];
    services.projectEntitlements.cloudCompileCapabilities = async () => ({
      enabled: false,
      allowedEngines: ['pdflatex', 'xelatex', 'lualatex'],
      maxFiles: 450,
      maxProjectBytes: 25_000_000,
      maxCompileSeconds: 120,
      maxOutputBytes: 60_000_000,
      concurrency: 2,
      dailyAllowance: 0,
      monthlyAllowance: 0,
      freeFallbackEnabled: true,
    });
    services.projectEntitlements.cloudCompile = async (input) => {
      cloudRequests.push(input);
      return {
        success: true,
        pdf: new Uint8Array([37, 80, 68, 70, 99]),
        synctex: new TextEncoder().encode('SyncTeX Version:1\nContent:\nPostamble:'),
        log: 'cloud live ok',
        errors: [],
        effectiveEngine: input.engine,
        engineFallbackUsed: false,
      };
    };

    const state = await services.projectLiveDraft.enableLivePdf('live-cloud-sticky');
    expect(state).toMatchObject({
      status: 'ready',
      enabled: true,
      certified: true,
      backend: 'cloud',
      rootFile: 'main.tex',
    });
    expect(state.pdf?.byteLength).toBeGreaterThan(0);
    expect(state.synctex?.byteLength).toBeGreaterThan(0);
    expect(liveCompiler.probeRequests).toHaveLength(0);
    expect(liveCompiler.renderRequests).toHaveLength(0);
    expect(cloudRequests).toHaveLength(1);
    expect(cloudRequests[0]).toMatchObject({ fallback: true, engine: 'pdflatex' });
    expect(env.store.get(services.projectCompile.$state).status).toBe('idle');
  });

  it('falls back generically on a missing browser font and keeps later Live PDF refreshes on Docker', async () => {
    const liveCompiler = new FakeLiveDraftCompiler();
    liveCompiler.render = async (request) => {
      liveCompiler.renderRequests.push(request);
      return {
        ok: false,
        engine: 'lualatex',
        runtime: 'busytex',
        log: '! error: (file generated-font-7)(type 3): font generated-font-7 at 600 not found !=>',
        durationMs: 18,
        reason: 'Live PDF compile failed: browser font resource unavailable.',
      };
    };
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-generic-font-fallback');
    const cloudRequests: Array<{ fallback?: boolean; engine: string }> = [];
    services.projectEntitlements.cloudCompileCapabilities = async () => ({
      enabled: false,
      allowedEngines: ['pdflatex', 'xelatex', 'lualatex'],
      maxFiles: 450,
      maxProjectBytes: 25_000_000,
      maxCompileSeconds: 120,
      maxOutputBytes: 60_000_000,
      concurrency: 2,
      dailyAllowance: 0,
      monthlyAllowance: 0,
      freeFallbackEnabled: true,
    });
    services.projectEntitlements.cloudCompile = async (input) => {
      cloudRequests.push(input);
      return {
        success: true,
        pdf: new Uint8Array([37, 80, 68, 70, 77, cloudRequests.length]),
        synctex: new TextEncoder().encode(`SyncTeX Version:1\nContent:${cloudRequests.length}\nPostamble:`),
        log: 'docker full texlive live pdf ok',
        errors: [],
        effectiveEngine: input.engine,
        engineFallbackUsed: false,
      };
    };

    const first = await services.projectLiveDraft.enableLivePdf('live-generic-font-fallback');
    expect(first).toMatchObject({ status: 'ready', backend: 'cloud', enabled: true, certified: true });
    expect(liveCompiler.probeRequests).toHaveLength(1);
    expect(liveCompiler.renderRequests).toHaveLength(1);
    expect(cloudRequests).toHaveLength(1);
    expect(await services.projectCompile.getProjectCloudFallbackSticky('live-generic-font-fallback')).toBe(true);

    services.latexSource.update(
      'live-generic-font-fallback:main.tex',
      '\documentclass{article}\n\begin{document}\nCloud refresh\n\end{document}\n',
      'source',
    );
    await new Promise((resolve) => setTimeout(resolve, 1_400));

    expect(liveCompiler.renderRequests).toHaveLength(1);
    expect(cloudRequests.length).toBeGreaterThanOrEqual(2);
    expect(env.store.get(services.projectLiveDraft.$livePdfState).backend).toBe('cloud');
  });

  it('R5 fails closed when the independent runtime certificate has no PDF payload', async () => {
    const liveCompiler = new FakeLiveDraftCompiler();
    liveCompiler.probe = async (request) => {
      liveCompiler.probeRequests.push(request);
      const { pdf: _pdf, ...withoutPdf } = SUCCESS_RESULT;
      return withoutPdf;
    };
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-pdf-no-pdf');

    const state = await services.projectLiveDraft.enableLivePdf('live-pdf-no-pdf');
    expect(state).toMatchObject({ status: 'error', enabled: false, certified: false });
    expect(liveCompiler.renderRequests).toHaveLength(0);
    expect(env.store.get(services.projectCompile.$state).status).toBe('idle');
  });

  it('keeps manuscript compile errors separate from R4 and recoverable on the next edit', async () => {
    const liveCompiler = new FakeLiveDraftCompiler();
    liveCompiler.render = async (request) => {
      liveCompiler.renderRequests.push(request);
      return {
        ok: false,
        engine: 'lualatex',
        runtime: 'busytex',
        log: '! Undefined control sequence.',
        durationMs: 8,
        reason: 'Live PDF compile failed.\n! Undefined control sequence.',
      };
    };
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectLiveDraft: () => ({ createLiveDraftCompiler: () => liveCompiler }),
      },
    });
    const services = await createWorkspace(env, 'live-pdf-error');

    const state = await services.projectLiveDraft.enableLivePdf('live-pdf-error');
    expect(state).toMatchObject({ status: 'error', enabled: true, certified: true });
    expect(state.error).toContain('Undefined control sequence');
    expect(liveCompiler.probeRequests).toHaveLength(1);
    expect(liveCompiler.renderRequests).toHaveLength(1);
  });
});
