import type {
  LatexCompileRequest,
  LatexCompileResult,
  LatexCompiler,
} from '@learnitive/latex-compiler';
import { projectFilePath } from '@learnitive/project-storage';
import { describe, expect, it } from 'vitest';
import {
  BusyTexLiveLuaCompiler,
  LEARNITIVE_LIVE_DRAFT_DIR,
  LEARNITIVE_LIVE_DRAFT_LUA_NAME,
  LEARNITIVE_LIVE_DRAFT_RECORD_NAME,
  LEARNITIVE_LIVE_DRAFT_WRAPPER_NAME,
} from '../busytex-live-lua-compiler';

class FakeBusyCompiler implements LatexCompiler {
  readonly backend = 'busytex' as const;
  requests: LatexCompileRequest[] = [];
  cancelled = false;
  disposed = false;
  probeSuccess = true;
  probePdf: Uint8Array | undefined = new Uint8Array([37, 80, 68, 70]);
  manuscriptSuccess = true;
  manuscriptPdf: Uint8Array | undefined = new Uint8Array([37, 80, 68, 70]);
  manuscriptLog = 'manuscript ok';
  probeArtifacts: LatexCompileResult['artifacts'] = [
    {
      path: `${LEARNITIVE_LIVE_DRAFT_DIR}/${LEARNITIVE_LIVE_DRAFT_RECORD_NAME}`,
      kind: 'other' as const,
      generated: true,
      text: true,
      textContent: [
        '{"type":"runtime","name":"directlua"}',
        '{"type":"page","page":1}',
        '{"type":"page","page":2}',
      ].join('\n'),
    },
  ];

  supports(compiler: 'pdflatex' | 'xelatex' | 'lualatex'): boolean {
    return compiler === 'lualatex';
  }

  async initialize(): Promise<void> {}

  async compile(request: LatexCompileRequest): Promise<LatexCompileResult> {
    this.requests.push(request);
    const isProbe = String(request.rootFile).startsWith(`${LEARNITIVE_LIVE_DRAFT_DIR}/`);
    const success = isProbe ? this.probeSuccess : this.manuscriptSuccess;
    const pdf = isProbe ? this.probePdf : this.manuscriptPdf;
    return {
      success,
      backend: 'busytex',
      compiler: 'lualatex',
      compileMode: 'full',
      exitCode: success ? 0 : 1,
      ...(pdf ? { pdf } : {}),
      log: isProbe ? 'probe ok' : this.manuscriptLog,
      diagnostics: [],
      runtimeDiagnostics: {
        status: 'ready',
        backend: 'busytex',
        serviceWorker: 'controlled',
        packageResolutionChain: [],
        artifactReadback: {
          attempts: [{ path: '<default>', ok: true, fileCount: this.probeArtifacts?.length ?? 0 }],
          mergedFileCount: this.probeArtifacts?.length ?? 0,
        },
      },
      durationMs: 12,
      bibliography: { bibtex: false, biberRequested: false, makeindex: false, rerun: false },
      ...(isProbe && this.probeArtifacts ? { artifacts: this.probeArtifacts } : {}),
    };
  }

  cancel(): void {
    this.cancelled = true;
  }

  dispose(): void {
    this.disposed = true;
  }
}

describe('BusyTexLiveLuaCompiler R4/R5 boundary', () => {
  it('certifies BusyTeX with a self-contained internal document and MEMFS shipout evidence', async () => {
    const fake = new FakeBusyCompiler();
    const compiler = new BusyTexLiveLuaCompiler(() => fake);

    const result = await compiler.probe({});

    expect(result.ok).toBe(true);
    expect(result.directLua).toBe(true);
    expect(result.memfsRecords).toBe(true);
    expect(result.pageCount).toBe(2);
    expect(result.hasMultiplePages).toBe(true);
    expect(result.pdf?.byteLength).toBeGreaterThan(0);
    const request = fake.requests[0];
    expect(request?.compiler).toBe('lualatex');
    expect(request?.auxiliaryPasses).toBe('none');
    // R4 still needs generated-artifact readback for its Lua/MEMFS certificate.
    expect(request?.artifactReadback).toBeUndefined();
    expect(request?.rootFile).toBe(
      projectFilePath(`${LEARNITIVE_LIVE_DRAFT_DIR}/${LEARNITIVE_LIVE_DRAFT_WRAPPER_NAME}`),
    );
    expect(request?.files).toHaveLength(2);
    expect(request?.files.some((file) => file.path === projectFilePath('main.tex'))).toBe(false);
    const probe = request?.files.find(
      (file) => file.path === projectFilePath(`${LEARNITIVE_LIVE_DRAFT_DIR}/${LEARNITIVE_LIVE_DRAFT_WRAPPER_NAME}`),
    );
    const luaProbe = request?.files.find(
      (file) => file.path === projectFilePath(`${LEARNITIVE_LIVE_DRAFT_DIR}/${LEARNITIVE_LIVE_DRAFT_LUA_NAME}`),
    );
    const luaSource = new TextDecoder().decode(luaProbe?.content);
    expect(new TextDecoder().decode(probe?.content)).toContain('\\directlua');
    expect(new TextDecoder().decode(probe?.content)).toContain(LEARNITIVE_LIVE_DRAFT_LUA_NAME);
    expect(luaSource).toContain('out:write(line, "\\n")');
    expect(luaSource).not.toContain('out:write(line, "\\\\n")');
    expect(new TextDecoder().decode(probe?.content)).not.toContain('\\input{main.tex}');
    expect(new TextDecoder().decode(probe?.content)).toContain('enddocument/afterlastpage');
    expect(result.certificate).toMatchObject({
      compileSucceeded: true,
      recordArtifactFound: true,
      directLuaRecordFound: true,
      pageRecords: 2,
      pdfBytes: 4,
    });
    expect(result.certificate.artifactReadbackAttempts).toHaveLength(1);
  });

  it('accepts the malformed literal-backslash-n R31 record as a compatibility fallback', async () => {
    const fake = new FakeBusyCompiler();
    fake.probeArtifacts = [{
      path: `${LEARNITIVE_LIVE_DRAFT_DIR}/${LEARNITIVE_LIVE_DRAFT_RECORD_NAME}`,
      kind: 'other',
      generated: true,
      text: true,
      textContent:
        '{"type":"runtime","name":"directlua"}\\n{"type":"page","page":1}\\n{"type":"page","page":2}\\n',
    }];

    const result = await new BusyTexLiveLuaCompiler(() => fake).probe({});

    expect(result.ok).toBe(true);
    expect(result.directLua).toBe(true);
    expect(result.pageCount).toBe(2);
    expect(result.records.map((record) => record.type)).toEqual(['runtime', 'page', 'page']);
  });

  it('reports the failing R4 certificate component instead of collapsing partial failures', async () => {
    const missingRecord = new FakeBusyCompiler();
    missingRecord.probeArtifacts = undefined;
    const noRecord = await new BusyTexLiveLuaCompiler(() => missingRecord).probe({});
    expect(noRecord.ok).toBe(false);
    expect(noRecord.reason).toBe('LuaLaTeX succeeded but the MEMFS runtime record could not be read.');
    expect(noRecord.certificate.pdfBytes).toBe(4);

    const missingPdf = new FakeBusyCompiler();
    missingPdf.probePdf = undefined;
    const noPdf = await new BusyTexLiveLuaCompiler(() => missingPdf).probe({});
    expect(noPdf.ok).toBe(false);
    expect(noPdf.reason).toBe('Lua/MEMFS shipout evidence passed, but BusyTeX returned no PDF bytes.');
    expect(noPdf.certificate.pageRecords).toBe(2);

    const missingShipout = new FakeBusyCompiler();
    missingShipout.probeArtifacts = [{
      path: LEARNITIVE_LIVE_DRAFT_RECORD_NAME,
      kind: 'other',
      generated: true,
      text: true,
      textContent: '{"type":"runtime","name":"directlua"}',
    }];
    const noShipout = await new BusyTexLiveLuaCompiler(() => missingShipout).probe({});
    expect(noShipout.ok).toBe(false);
    expect(noShipout.reason).toBe('PDF bytes were returned, but no Lua/MEMFS shipout record was visible.');
  });

  it('renders the canonical manuscript directly after certification without the R4 wrapper', async () => {
    const fake = new FakeBusyCompiler();
    const compiler = new BusyTexLiveLuaCompiler(() => fake);
    await compiler.probe({});
    const source = new TextEncoder().encode('\\documentclass{article}\\begin{document}Hello\\end{document}');

    const result = await compiler.render({
      rootFile: projectFilePath('paper/main.tex'),
      files: [
        { path: projectFilePath('paper/main.tex'), content: source },
        { path: projectFilePath('paper/sections/body.tex'), content: new TextEncoder().encode('Body') },
      ],
    });

    expect(result.ok).toBe(true);
    expect(result.pdf?.byteLength).toBeGreaterThan(0);
    const renderRequest = fake.requests.at(-1);
    expect(renderRequest?.rootFile).toBe(projectFilePath('paper/main.tex'));
    expect(renderRequest?.auxiliaryPasses).toBe('none');
    expect(renderRequest?.artifactReadback).toBe('none');
    expect(renderRequest?.files.some((file) => file.path === projectFilePath('paper/main.tex'))).toBe(true);
    expect(renderRequest?.files.some((file) => String(file.path).includes(LEARNITIVE_LIVE_DRAFT_WRAPPER_NAME))).toBe(false);
  });

  it('R6 renders only the edited block on a separate warm BusyTeX lane', async () => {
    const created: FakeBusyCompiler[] = [];
    const compiler = new BusyTexLiveLuaCompiler(() => {
      const fake = new FakeBusyCompiler();
      created.push(fake);
      return fake;
    });
    await compiler.probe({});
    await compiler.warmInstant();
    const root = String.raw`\documentclass{article}
\usepackage{amsmath}
\begin{document}
Old full manuscript body.
\end{document}
`;
    const result = await compiler.renderInstant({
      rootFile: projectFilePath('paper/main.tex'),
      files: [{ path: projectFilePath('paper/main.tex'), content: new TextEncoder().encode(root) }],
      text: 'Edited paragraph appears immediately.',
      hsize: 240,
    });

    expect(result.ok).toBe(true);
    expect(created).toHaveLength(2);
    const exactLane = created[0];
    const instantLane = created[1];
    expect(instantLane?.requests).toHaveLength(2);
    const warmRequest = instantLane?.requests[0];
    expect(warmRequest?.rootFile).toBe(projectFilePath('__learnitive_live_instant_warm__.tex'));
    expect(warmRequest?.artifactReadback).toBe('none');
    const request = instantLane?.requests.at(-1);
    expect(request?.rootFile).toBe(projectFilePath('paper/__learnitive_live_instant__.tex'));
    expect(request?.mode).toBe('fast');
    expect(request?.auxiliaryPasses).toBe('none');
    expect(request?.artifactReadback).toBe('none');
    const wrapper = request?.files.find((file) => file.path === request.rootFile);
    const source = new TextDecoder().decode(wrapper?.content);
    expect(source).toContain(String.raw`\usepackage{amsmath}`);
    expect(source).toContain('Edited paragraph appears immediately.');
    expect(source).not.toContain('Old full manuscript body.');
    expect(source).toContain('240.000pt');

    compiler.cancelInstant();
    expect(instantLane?.cancelled).toBe(true);
    expect(exactLane?.cancelled).toBe(false);
  });

  it('surfaces the useful LaTeX diagnostic instead of Emscripten runtime-noise tails', async () => {
    const fake = new FakeBusyCompiler();
    fake.manuscriptSuccess = false;
    fake.manuscriptPdf = undefined;
    fake.manuscriptLog = [
      "! LaTeX Error: File `missing-image.png' not found.",
      'l.17 \\includegraphics{missing-image.png}',
      '27 hlist, 1 vlist, 3 rule, 28 disc, 6 local_par, 3 dir, 2 math, 166 glue',
      'Fatal error occurred, no output PDF file produced!',
      'STDERR: program exited (with status: 1), but keepRuntimeAlive() is set (counter=0)',
      'due to an async operation, so halting execution but not exiting the runtime',
    ].join('\n');

    const result = await new BusyTexLiveLuaCompiler(() => fake).render({
      rootFile: projectFilePath('main.tex'),
      files: [{ path: projectFilePath('main.tex'), content: new TextEncoder().encode('bad') }],
    });

    expect(result.ok).toBe(false);
    expect(result.reason).toContain("LaTeX Error: File `missing-image.png' not found");
    expect(result.reason).toContain('includegraphics');
    expect(result.reason).not.toContain('hlist');
    expect(result.reason).not.toContain('keepRuntimeAlive');
    expect(result.log).toContain('keepRuntimeAlive');
  });

  it('owns cancellation and disposal independently of the manual compiler service', async () => {
    const fake = new FakeBusyCompiler();
    const compiler = new BusyTexLiveLuaCompiler(() => fake);
    compiler.cancel();
    expect(fake.cancelled).toBe(false);
    await compiler.probe({});
    compiler.cancel();
    expect(fake.cancelled).toBe(true);
    compiler.dispose();
    expect(fake.disposed).toBe(true);
  });
});
