import { WORKSPACE_STORAGE_TYPE } from '@learnitive/constants';
import type {
  LatexCompileRequest,
  LatexCompileResult,
  LatexCompiler,
  LatexCompilerCapabilities,
} from '@learnitive/latex-compiler';
import { projectFilePath } from '@learnitive/project-storage';
import { LOCAL_AI_MODELS } from '@learnitive/project-ai';
import { createTestEnvironment } from '@learnitive/test-utils';
import { afterEach, describe, expect, it } from 'vitest';
import { fullTexliveFallbackReasonFromText } from '../project-compile-service';

class FakeCompiler implements LatexCompiler {
  readonly backend = 'busytex' as const;
  requests: LatexCompileRequest[] = [];
  initializeCalls = 0;

  supports(_compiler: 'pdflatex' | 'xelatex' | 'lualatex'): boolean {
    return true;
  }
  async initialize(): Promise<void> {
    this.initializeCalls += 1;
  }
  async compile(request: LatexCompileRequest): Promise<LatexCompileResult> {
    this.requests.push(request);
    return {
      success: true,
      backend: this.backend,
      compiler: request.compiler,
      exitCode: 0,
      pdf: new Uint8Array([37, 80, 68, 70]),
      synctex: new Uint8Array([31, 139]),
      log: 'ok',
      diagnostics: [],
      durationMs: 12,
      bibliography: {
        bibtex: false,
        biberRequested: false,
        makeindex: false,
        rerun: true,
      },
    };
  }
  cancel(): void {}
  dispose(): void {}
}



class MissingPackageCompiler extends FakeCompiler {
  override async compile(request: LatexCompileRequest): Promise<LatexCompileResult> {
    this.requests.push(request);
    return {
      success: false,
      backend: this.backend,
      compiler: request.compiler,
      exitCode: 1,
      log: "! LaTeX Error: File `inconsolata.sty' not found.",
      diagnostics: [{ severity: 'error', message: "File `inconsolata.sty' not found." }],
      durationMs: 8,
      bibliography: {
        bibtex: false,
        biberRequested: false,
        makeindex: false,
        rerun: false,
      },
    };
  }
}

class CapabilityFailureCompiler extends FakeCompiler {
  async getCapabilities(): Promise<LatexCompilerCapabilities> {
    throw new Error('BusyTeX capability bootstrap failed');
  }
}

class PdfLatexOnlyCompiler extends FakeCompiler {
  override supports(compiler: 'pdflatex' | 'xelatex' | 'lualatex'): boolean {
    return compiler === 'pdflatex';
  }

  async getCapabilities(): Promise<LatexCompilerCapabilities> {
    return {
      schemaVersion: 1,
      mode: 'swiftlatex-pdflatex',
      engines: {
        pdflatex: {
          available: true,
          preferredBackend: 'swiftlatex',
          reason: null,
        },
        xelatex: {
          available: false,
          preferredBackend: null,
          reason: 'XeLaTeX requires BusyTeX.',
        },
        lualatex: {
          available: false,
          preferredBackend: null,
          reason: 'LuaLaTeX requires BusyTeX.',
        },
      },
      texlive: {
        bundled: {
          available: true,
          bundleId: 'test',
          compatibility: 'swiftlatex-r3',
          fileCount: 1,
          textFileCount: 1,
          binaryFileCount: 0,
        },
        staticMirror: { enabled: false, endpoint: null, index: null },
        remote: { enabled: false, endpoint: null, configurable: true },
        busytexEndpoint: null,
      },
    };
  }
}

describe('ProjectCompileService', () => {
  const controllers: AbortController[] = [];

  it('classifies generic missing browser font/runtime resources without treating source errors as fallback conditions', () => {
    expect(
      fullTexliveFallbackReasonFromText(
        '! error: (file generated-font-7)(type 3): font generated-font-7 at 600 not found !=>',
      ),
    ).toBe('browser TeX Live package, font, bibliography or runtime limitation');
    expect(
      fullTexliveFallbackReasonFromText(
        'luaotfload | resolve : sequence of 3 lookups yielded nothing appropriate for Example Serif',
      ),
    ).toBe('browser TeX Live package, font, bibliography or runtime limitation');
    expect(fullTexliveFallbackReasonFromText('! Undefined control sequence.')).toBeUndefined();
  });
  afterEach(() => {
    for (const controller of controllers.splice(0)) controller.abort();
  });


  it('reads deployment capabilities without initializing or compiling a hidden test document', async () => {
    const compiler = new PdfLatexOnlyCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectCompile: () => ({ createCompiler: () => compiler }),
      },
    });
    const services = env.instantiateAll();
    await env.mountAll();

    const capabilities = await services.projectCompile.getCompilerCapabilities();

    expect(capabilities.engines.pdflatex.available).toBe(true);
    expect(compiler.initializeCalls).toBe(0);
    expect(compiler.requests).toHaveLength(0);
  });

  it('compiles a binary-safe project snapshot and retains PDF/SyncTeX in memory', async () => {
    const compiler = new FakeCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectCompile: () => ({ createCompiler: () => compiler }),
      },
    });
    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'),
      new TextEncoder().encode('\\documentclass{article}'),
    );
    await storage.writeFile(
      projectFilePath('figures/result.pdf'),
      new Uint8Array([0, 255, 12]),
    );

    const result = await services.projectCompile.compile('paper');
    expect(result.success).toBe(true);
    expect(result.synctex).toEqual(new Uint8Array([31, 139]));
    expect(services.projectCompile.getLastSuccessful('paper')?.pdf).toEqual(
      new Uint8Array([37, 80, 68, 70]),
    );
    expect(compiler.requests).toHaveLength(1);
    expect(compiler.requests[0]?.mode).toBe('full');
    expect(
      compiler.requests[0]?.files.find(
        (file) => file.path === 'figures/result.pdf',
      )?.content,
    ).toEqual(new Uint8Array([0, 255, 12]));
  });


  it('persists Fast Preview for pdfLaTeX and forwards it to the compiler', async () => {
    const compiler = new FakeCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectCompile: () => ({ createCompiler: () => compiler }),
      },
    });
    const services = env.instantiateAll();
    await env.mountAll();
    await services.workspaceOps.createWorkspaceInfo({
      name: 'fast-paper',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });
    const storage = await services.projectStorage.getProvider('fast-paper');
    await storage.writeFile(
      projectFilePath('main.tex'),
      new TextEncoder().encode('\\documentclass{article}'),
    );

    await services.projectCompile.setProjectPdfCompileMode('fast-paper', 'fast');
    expect(await services.projectCompile.getProjectPdfCompileMode('fast-paper')).toBe('fast');
    await services.projectCompile.compile('fast-paper');
    expect(compiler.requests.at(-1)?.mode).toBe('fast');
  });

  it('routes Full / SyncTeX to isolated TeX Live when the selected engine is unavailable in BusyTeX', async () => {
    const compiler = new PdfLatexOnlyCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectCompile: () => ({ createCompiler: () => compiler }),
      },
    });
    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'),
      new TextEncoder().encode('\\documentclass{article}'),
    );
    await services.projectStorage.updateProjectConfig('paper', (current) => ({
      ...current,
      compiler: 'xelatex',
      pdfCompileMode: 'full',
    }));

    const cloudRequests: Array<{ engine: string; fallback?: boolean }> = [];
    // Deliberately advertise disabled capability metadata. The compile endpoint
    // is authoritative and must still be attempted for Full / SyncTeX.
    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: false,
    });
    services.projectEntitlements.cloudCompile = async (input) => {
      cloudRequests.push({ engine: input.engine, fallback: input.fallback });
      return {
        success: true,
        pdf: new Uint8Array([37, 80, 68, 70]),
        synctex: new Uint8Array([31, 139]),
        log: 'isolated xelatex ok',
        errors: [],
        effectiveEngine: input.engine,
        engineFallbackUsed: false,
      };
    };

    const result = await services.projectCompile.compile('paper');
    expect(result.success).toBe(true);
    expect(result.backend).toBe('cloud');
    expect(result.compiler).toBe('xelatex');
    expect(result.pdf?.byteLength).toBeGreaterThan(0);
    expect(result.synctex?.byteLength).toBeGreaterThan(0);
    expect(compiler.requests).toHaveLength(0);
    expect(cloudRequests).toEqual([{ engine: 'xelatex', fallback: true }]);
  });

  it('routes Full / SyncTeX to isolated TeX Live when browser capability initialization throws', async () => {
    const compiler = new CapabilityFailureCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectCompile: () => ({ createCompiler: () => compiler }),
      },
    });
    const services = env.instantiateAll();
    await env.mountAll();
    await services.workspaceOps.createWorkspaceInfo({
      name: 'capability-failure-paper',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });
    const storage = await services.projectStorage.getProvider('capability-failure-paper');
    await storage.writeFile(
      projectFilePath('main.tex'),
      new TextEncoder().encode('\\documentclass{article}'),
    );

    let cloudCalls = 0;
    services.projectEntitlements.cloudCompile = async (input) => {
      cloudCalls += 1;
      return {
        success: true,
        pdf: new Uint8Array([37, 80, 68, 70]),
        synctex: new Uint8Array([31, 139]),
        log: 'isolated fallback after browser capability failure',
        errors: [],
        effectiveEngine: input.engine,
        engineFallbackUsed: false,
      };
    };

    const result = await services.projectCompile.compile('capability-failure-paper');
    expect(result.success).toBe(true);
    expect(result.backend).toBe('cloud');
    expect(cloudCalls).toBe(1);
    expect(compiler.requests).toHaveLength(0);
    expect(result.log).toContain('browser compiler capability initialization failed');
  });

  it('persists Full / SyncTeX without requiring a BusyTeX capability preflight', async () => {
    const compiler = new PdfLatexOnlyCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectCompile: () => ({ createCompiler: () => compiler }),
      },
    });
    const services = env.instantiateAll();
    await env.mountAll();
    await services.workspaceOps.createWorkspaceInfo({
      name: 'full-intent-paper',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });

    let capabilityReads = 0;
    services.projectEntitlements.cloudCompileCapabilities = async () => {
      capabilityReads += 1;
      throw new Error('capability endpoint intentionally unavailable');
    };

    await services.projectCompile.setProjectPdfCompileMode('full-intent-paper', 'full');
    expect(await services.projectCompile.getProjectPdfCompileMode('full-intent-paper')).toBe('full');
    expect(capabilityReads).toBe(0);
    expect(compiler.requests).toHaveLength(0);
  });

  it('routes pdfLaTeX Full mode directly to isolated TeX Live when browser SyncTeX is unavailable', async () => {
    const compiler = new PdfLatexOnlyCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectCompile: () => ({ createCompiler: () => compiler }),
      },
    });
    const services = env.instantiateAll();
    await env.mountAll();
    await services.workspaceOps.createWorkspaceInfo({
      name: 'pdflatex-full-fallback',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });
    const storage = await services.projectStorage.getProvider('pdflatex-full-fallback');
    await storage.writeFile(
      projectFilePath('main.tex'),
      new TextEncoder().encode('\\documentclass{article}'),
    );
    await services.projectStorage.updateProjectConfig('pdflatex-full-fallback', (current) => ({
      ...current,
      compiler: 'pdflatex',
      pdfCompileMode: 'full',
    }));

    let cloudCalls = 0;
    services.projectEntitlements.cloudCompile = async (input) => {
      cloudCalls += 1;
      return {
        success: true,
        pdf: new Uint8Array([37, 80, 68, 70]),
        synctex: new Uint8Array([31, 139]),
        log: 'isolated pdflatex synctex ok',
        errors: [],
        effectiveEngine: input.engine,
        engineFallbackUsed: false,
      };
    };

    const result = await services.projectCompile.compile('pdflatex-full-fallback');
    expect(result.success).toBe(true);
    expect(result.backend).toBe('cloud');
    expect(cloudCalls).toBe(1);
    expect(compiler.requests).toHaveLength(0);
  });


  it('remembers a successful Docker reliability fallback per project and skips the browser on later compiles', async () => {
    const compiler = new MissingPackageCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectCompile: () => ({ createCompiler: () => compiler }),
      },
    });
    const services = env.instantiateAll();
    await env.mountAll();
    await services.workspaceOps.createWorkspaceInfo({
      name: 'sticky-cloud-paper',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });
    const storage = await services.projectStorage.getProvider('sticky-cloud-paper');
    await storage.writeFile(
      projectFilePath('main.tex'),
      new TextEncoder().encode('\\documentclass{article}'),
    );

    const cloudRequests: Array<{ fallback?: boolean }> = [];
    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, 45, cloudRequests.length]),
        synctex: new Uint8Array([31, 139, cloudRequests.length]),
        log: 'docker texlive full ok',
        errors: [],
        effectiveEngine: input.engine,
        engineFallbackUsed: false,
      };
    };

    const first = await services.projectCompile.compile('sticky-cloud-paper');
    expect(first.backend).toBe('cloud');
    expect(compiler.requests).toHaveLength(1);
    expect(cloudRequests).toHaveLength(1);
    expect(cloudRequests[0]?.fallback).toBe(true);
    expect(await services.projectCompile.getProjectCloudFallbackSticky('sticky-cloud-paper')).toBe(true);

    const second = await services.projectCompile.compile('sticky-cloud-paper');
    expect(second.backend).toBe('cloud');
    expect(compiler.requests).toHaveLength(1);
    expect(cloudRequests).toHaveLength(2);
    expect(second.log).toContain('remembered for this project');
  });

  it('keeps canonical source mutations independent from manual compilation', async () => {
    const compiler = new FakeCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectCompile: () => ({ createCompiler: () => compiler }),
      },
    });
    const services = env.instantiateAll();
    await env.mountAll();
    await services.workspaceOps.createWorkspaceInfo({
      name: 'manual-paper',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });
    const wsPath = 'manual-paper:main.tex';
    await services.fileSystem.createTextFile(
      wsPath,
      '\\documentclass{article}\n\\begin{document}\nA\n\\end{document}\n',
    );
    await services.latexSource.load(wsPath);

    services.latexSource.update(
      wsPath,
      '\\documentclass{article}\n\\begin{document}\nAB\n\\end{document}\n',
      'source',
    );
    services.latexSource.update(
      wsPath,
      '\\documentclass{article}\n\\begin{document}\nABC\n\\end{document}\n',
      'visual',
    );

    await Promise.resolve();
    expect(compiler.requests).toHaveLength(0);
    expect(env.store.get(services.projectCompile.$state).status).toBe('idle');

    await services.projectCompile.compile('manual-paper');
    expect(compiler.requests).toHaveLength(1);
    expect(compiler.requests[0]?.mode).toBe('full');
  });

  it('keeps AI Insert source changes manual-only until Compile is requested', async () => {
    const compiler = new FakeCompiler();
    const controller = new AbortController();
    controllers.push(controller);
    const env = createTestEnvironment({
      controller,
      coreConfigOverrides: {
        projectCompile: () => ({ createCompiler: () => compiler }),
      },
    });
    const services = env.instantiateAll();
    await env.mountAll();
    await services.workspaceOps.createWorkspaceInfo({
      name: 'ai-manual-paper',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });
    const wsPath = 'ai-manual-paper:main.tex';
    const source = '\\documentclass{article}\n\\begin{document}\nBefore\n\\end{document}\n';
    await services.fileSystem.createTextFile(wsPath, source);
    await services.latexSource.load(wsPath);
    services.projectAi.configure({
      enabled: true,
      provider: 'local',
      localModelId: LOCAL_AI_MODELS[0]!.id,
      model: '',
      rememberApiKey: false,
      privacyMode: true,
      autoExplainErrors: false,
    });
    services.projectAi.setEditorContext('ai-manual-paper', {
      path: projectFilePath('main.tex'),
      line: 4,
      column: 1,
      offset: source.indexOf('\\end{document}'),
    });

    await services.projectAi.send('ai-manual-paper', 'insert e=mc^2 equation at cursor');
    const snippet = services.projectAi.getWorkspaceState('ai-manual-paper').messages.at(-1)?.snippet;
    expect(snippet?.latex).toContain('e=mc^2');

    await services.projectAi.insertLatexSnippetAtCurrentEditor(
      'ai-manual-paper',
      snippet?.latex ?? '',
    );
    await Promise.resolve();
    expect(compiler.requests).toHaveLength(0);

    await services.projectCompile.compile('ai-manual-paper');
    expect(compiler.requests).toHaveLength(1);
    expect(compiler.requests[0]?.mode).toBe('full');
    const compiledMain = compiler.requests[0]?.files.find((file) => file.path === 'main.tex');
    expect(new TextDecoder().decode(compiledMain?.content)).toContain('e=mc^2');
  });

});
