import type {
  LatexCompileRequest,
  LatexCompileResult,
  LatexCompiler,
  LatexCompilerCapabilities,
} from '@learnitive/latex-compiler';
import { projectFilePath } from '@learnitive/project-storage';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { BrowserLatexCompiler } from '../browser-latex-compiler';

const capabilities: LatexCompilerCapabilities = {
  schemaVersion: 1,
  mode: 'busytex-with-swiftlatex-fallback',
  engines: {
    pdflatex: {
      available: true,
      preferredBackend: 'swiftlatex',
      reason: null,
      supportsSyncTeX: true,
      syncTeXBackend: 'busytex',
      supportsBiber: true,
    },
    xelatex: {
      available: true,
      preferredBackend: 'busytex',
      reason: null,
      supportsSyncTeX: true,
      syncTeXBackend: 'busytex',
      supportsBiber: true,
    },
    lualatex: {
      available: true,
      preferredBackend: 'busytex',
      reason: null,
      supportsSyncTeX: true,
      syncTeXBackend: 'busytex',
      supportsBiber: true,
    },
  },
  texlive: {
    bundled: {
      available: true,
      bundleId: 'test',
      compatibility: 'test',
      fileCount: 1,
      textFileCount: 1,
      binaryFileCount: 0,
    },
    staticMirror: { enabled: false, endpoint: null, index: null },
    remote: { enabled: false, endpoint: null, configurable: true },
    busytexEndpoint: null,
  },
};

class FakeCompiler implements LatexCompiler {
  requests: LatexCompileRequest[] = [];

  constructor(
    readonly backend: 'busytex' | 'swiftlatex',
    private readonly failure?: Error,
  ) {}

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

  async initialize(): Promise<void> {}

  async compile(request: LatexCompileRequest): Promise<LatexCompileResult> {
    this.requests.push(request);
    if (this.failure) throw this.failure;
    return {
      success: true,
      backend: this.backend,
      compiler: request.compiler,
      compileMode: request.mode ?? 'full',
      exitCode: 0,
      pdf: new Uint8Array([37, 80, 68, 70]),
      ...(this.backend === 'busytex' ? { synctex: new Uint8Array([31, 139]) } : {}),
      log: 'ok',
      diagnostics: [],
      durationMs: 1,
      bibliography: {
        bibtex: false,
        biberRequested: false,
        makeindex: false,
        rerun: false,
      },
    };
  }

  cancel(): void {}
  dispose(): void {}
}

function request(mode: 'full' | 'fast'): LatexCompileRequest {
  return {
    compiler: 'pdflatex',
    mode,
    rootFile: projectFilePath('main.tex'),
    files: [
      {
        path: projectFilePath('main.tex'),
        content: new TextEncoder().encode('\\documentclass{article}'),
      },
    ],
  };
}

describe('R14.22.2 BrowserLatexCompiler compile modes', () => {
  afterEach(() => vi.unstubAllGlobals());

  it('uses BusyTeX for Full / SyncTeX and SwiftLaTeX for Fast Preview', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn(async () => new Response(JSON.stringify(capabilities), { status: 200 })),
    );
    const primary = new FakeCompiler('busytex');
    const fallback = new FakeCompiler('swiftlatex');
    const compiler = new BrowserLatexCompiler(primary, fallback);

    await expect(compiler.compile(request('full'))).resolves.toMatchObject({
      backend: 'busytex',
      compileMode: 'full',
    });
    await expect(compiler.compile(request('fast'))).resolves.toMatchObject({
      backend: 'swiftlatex',
      compileMode: 'fast',
    });
    expect(primary.requests).toHaveLength(1);
    expect(fallback.requests).toHaveLength(1);
  });

  it('forces Fast Preview through SwiftLaTeX even when deployment prefers BusyTeX', async () => {
    const busyPreferred = structuredClone(capabilities);
    busyPreferred.engines.pdflatex.preferredBackend = 'busytex';
    vi.stubGlobal(
      'fetch',
      vi.fn(async () => new Response(JSON.stringify(busyPreferred), { status: 200 })),
    );
    const primary = new FakeCompiler('busytex');
    const fallback = new FakeCompiler('swiftlatex');
    const compiler = new BrowserLatexCompiler(primary, fallback);

    await expect(compiler.compile(request('fast'))).resolves.toMatchObject({
      backend: 'swiftlatex',
      compileMode: 'fast',
    });
    expect(primary.requests).toHaveLength(0);
    expect(fallback.requests).toHaveLength(1);
  });

  it('never silently falls back to SwiftLaTeX when Full / SyncTeX BusyTeX fails', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn(async () => new Response(JSON.stringify(capabilities), { status: 200 })),
    );
    const primary = new FakeCompiler(
      'busytex',
      new Error('Failed to initialize BusyTeX runtime worker'),
    );
    const fallback = new FakeCompiler('swiftlatex');
    const compiler = new BrowserLatexCompiler(primary, fallback);

    await expect(compiler.compile(request('full'))).rejects.toThrow(
      'Switch this project to Fast Preview',
    );
    expect(fallback.requests).toHaveLength(0);
  });
  it('promotes biblatex/Biber projects to BusyTeX Full mode instead of SwiftLaTeX Fast Preview', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn(async () => new Response(JSON.stringify(capabilities), { status: 200 })),
    );
    const primary = new FakeCompiler('busytex');
    const fallback = new FakeCompiler('swiftlatex');
    const compiler = new BrowserLatexCompiler(primary, fallback);
    const biberRequest = request('fast');
    biberRequest.files = [
      {
        path: projectFilePath('main.tex'),
        content: new TextEncoder().encode(
          '\\documentclass{article}\n\\usepackage[backend=biber]{biblatex}\n\\addbibresource{refs.bib}',
        ),
      },
      {
        path: projectFilePath('refs.bib'),
        content: new TextEncoder().encode('@book{sample,title={Sample},author={Author},year={2026}}'),
      },
    ];

    await expect(compiler.compile(biberRequest)).resolves.toMatchObject({
      backend: 'busytex',
      compileMode: 'full',
    });
    expect(primary.requests).toHaveLength(1);
    expect(primary.requests[0]?.mode).toBe('full');
    expect(fallback.requests).toHaveLength(0);
  });

});
