import { WORKSPACE_STORAGE_TYPE } from '@learnitive/constants';
import {
  createDefaultLearnitiveProjectConfig,
  DEFAULT_LEARNITIVE_MAIN_TEX,
  initializeBlankLearnitiveProject,
  projectFilePath,
} from '@learnitive/project-storage';
import { createTestEnvironment } from '@learnitive/test-utils';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

describe('ProjectStorageService', () => {
  let controller: AbortController;

  beforeEach(() => {
    controller = new AbortController();
  });

  afterEach(() => {
    controller.abort();
  });

  it('keeps project storage binary-safe and round-trips a whole project ZIP', async () => {
    const testEnv = createTestEnvironment({ controller });
    const services = testEnv.instantiateAll();
    await testEnv.mountAll();

    const projectConfig = {
      ...createDefaultLearnitiveProjectConfig(),
      compiler: 'xelatex' as const,
      assets: { imageDirectory: 'media/' },
    };
    await services.workspaceOps.createWorkspaceInfo({
      name: 'source',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: { project: projectConfig },
    });
    await services.workspaceOps.createWorkspaceInfo({
      name: 'target',
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });

    const source = await services.projectStorage.getProvider('source');
    await source.writeFile(
      projectFilePath('main.tex'),
      new TextEncoder().encode('\\documentclass{article}'),
    );
    const binary = new Uint8Array([0, 255, 7, 42]);
    await source.writeFile(projectFilePath('figures/chart.png'), binary);
    await source.writeFile(
      projectFilePath('Makefile'),
      new TextEncoder().encode('all:'),
    );
    const importedAsset = await services.projectAssets.importImage('source', {
      fileName: 'Result Plot.PNG',
      data: new Uint8Array([3, 1, 4]),
    });
    expect(importedAsset.path).toBe('media/Result-Plot.png');

    await expect(
      services.projectStorage.getProjectConfig('source'),
    ).resolves.toEqual(projectConfig);

    const archive = await services.projectStorage.exportZip('source');
    const imported = await services.projectStorage.importZip('target', archive);
    expect(new Set(imported.map(String))).toEqual(
      new Set([
        'Makefile',
        'figures/chart.png',
        'main.tex',
        'media/Result-Plot.png',
      ]),
    );

    const target = await services.projectStorage.getProvider('target');
    await expect(
      target.readFile(projectFilePath('figures/chart.png')),
    ).resolves.toEqual(binary);
    await expect(
      services.projectStorage.getProjectConfig('target'),
    ).resolves.toEqual(createDefaultLearnitiveProjectConfig());
  });

  it('R14.10 replaces only untouched blank-project scaffolding during whole-project ZIP import', async () => {
    const testEnv = createTestEnvironment({ controller });
    const services = testEnv.instantiateAll();
    await testEnv.mountAll();

    for (const name of ['portable-source', 'portable-target']) {
      await services.workspaceOps.createWorkspaceInfo({
        name,
        type: WORKSPACE_STORAGE_TYPE.Memory,
        metadata: {},
      });
    }

    const source = await services.projectStorage.getProvider('portable-source');
    await source.writeFile(
      projectFilePath('main.tex'),
      new TextEncoder().encode('\\documentclass{article}\n\\begin{document}\nPortable R14.10\n\\end{document}\n'),
    );
    await source.writeFile(
      projectFilePath('references.bib'),
      new TextEncoder().encode('@article{portable,title={Portable}}\n'),
    );

    const target = await services.projectStorage.getProvider('portable-target');
    await initializeBlankLearnitiveProject(target);
    await expect(target.readFile(projectFilePath('main.tex'))).resolves.toEqual(
      new TextEncoder().encode(DEFAULT_LEARNITIVE_MAIN_TEX),
    );

    const archive = await services.projectStorage.exportZip('portable-source');
    await expect(
      services.projectStorage.importZip('portable-target', archive),
    ).resolves.toHaveLength(2);
    await expect(target.readFile(projectFilePath('main.tex'))).resolves.toEqual(
      new TextEncoder().encode('\\documentclass{article}\n\\begin{document}\nPortable R14.10\n\\end{document}\n'),
    );
  });

  it('R14.10 never overwrites a modified root file and restores blank scaffolding after a failed import', async () => {
    const testEnv = createTestEnvironment({ controller });
    const services = testEnv.instantiateAll();
    await testEnv.mountAll();

    for (const name of ['modified-target', 'restore-target']) {
      await services.workspaceOps.createWorkspaceInfo({
        name,
        type: WORKSPACE_STORAGE_TYPE.Memory,
        metadata: {},
      });
    }

    const modified = await services.projectStorage.getProvider('modified-target');
    await initializeBlankLearnitiveProject(modified);
    const modifiedSource = new TextEncoder().encode(`${DEFAULT_LEARNITIVE_MAIN_TEX}% user edit\n`);
    await modified.writeFile(projectFilePath('main.tex'), modifiedSource);

    const archiveSourceName = 'archive-source';
    await services.workspaceOps.createWorkspaceInfo({
      name: archiveSourceName,
      type: WORKSPACE_STORAGE_TYPE.Memory,
      metadata: {},
    });
    const archiveSource = await services.projectStorage.getProvider(archiveSourceName);
    await archiveSource.writeFile(projectFilePath('main.tex'), new TextEncoder().encode('replacement'));
    const archive = await services.projectStorage.exportZip(archiveSourceName);

    await expect(
      services.projectStorage.importZip('modified-target', archive),
    ).rejects.toThrow(/existing project file: main\.tex/);
    await expect(modified.readFile(projectFilePath('main.tex'))).resolves.toEqual(modifiedSource);

    const restore = await services.projectStorage.getProvider('restore-target');
    await initializeBlankLearnitiveProject(restore);
    const originalWriteFile = restore.writeFile.bind(restore);
    let rejectedReplacement = false;
    restore.writeFile = async (path, data, signal) => {
      if (
        String(path) === 'main.tex' &&
        new TextDecoder().decode(data) === 'replacement' &&
        !rejectedReplacement
      ) {
        rejectedReplacement = true;
        throw new Error('simulated storage write failure');
      }
      await originalWriteFile(path, data, signal);
    };
    await expect(
      services.projectStorage.importZip('restore-target', archive),
    ).rejects.toThrow('simulated storage write failure');
    await expect(restore.readFile(projectFilePath('main.tex'))).resolves.toEqual(
      new TextEncoder().encode(DEFAULT_LEARNITIVE_MAIN_TEX),
    );
  });

});
