import { describe, expect, it } from 'vitest';
import { MemoryProjectStorage } from '../memory-project-storage';
import { projectFilePath } from '../project-path';
import {
  fetchProjectTemplateFiles,
  instantiateProjectTemplate,
  loadProjectTemplateCategories,
  loadProjectTemplateManifest,
  type ProjectTemplateDefinition,
} from '../project-templates';

const decoder = new TextDecoder();
const encoder = new TextEncoder();

function template(): ProjectTemplateDefinition {
  return {
    id: 'research-paper',
    slug: 'research-paper',
    name: 'Research Paper',
    description: 'Test template',
    category: 'Academic',
    author: 'Learnitive',
    tier: 'free',
    tags: ['research'],
    degreeLevels: [],
    requiredPackages: [],
    features: [],
    entryFile: projectFilePath('main.tex'),
    templateVersion: '1.0.0',
    totalBytes: encoder.encode('mainintro').length,
    files: [
      { path: projectFilePath('main.tex'), url: '/api/templates/research-paper/files/main.tex', size: 4 },
      { path: projectFilePath('sections/introduction.tex'), url: '/api/templates/research-paper/files/sections/introduction.tex', size: 5 },
    ],
    config: { version: 1, rootFile: 'main.tex', compiler: 'pdflatex', pdfCompileMode: 'full', assets: { imageDirectory: 'figures/' } },
  };
}

const fileFetcher = async (input: RequestInfo | URL) => {
  const url = String(input);
  const value = url.endsWith('main.tex') ? 'main' : 'intro';
  return new Response(encoder.encode(value), { status: 200, headers: { 'content-length': String(value.length) } });
};

describe('Learnitive project template manifest', () => {
  it('loads a public manifest instead of a hard-coded catalog', async () => {
    const manifestBody = {
      version: 1,
      generatedAt: '2026-08-20T00:00:00.000Z',
      contentHash: 'abc',
      templates: [{
        id: 'article', slug: 'article', name: 'Article', description: '', category: 'Academic', author: 'Learnitive', tier: 'free', tags: [], degreeLevels: [], requiredPackages: [], features: [],
        entryFile: 'main.tex', templateVersion: '1.0.0', totalBytes: 1, thumbnail: null, preview: null,
        files: [{ path: 'main.tex', url: '/api/templates/article/files/main.tex', size: 1 }],
      }],
    };
    const result = await loadProjectTemplateManifest(undefined, async () => new Response(JSON.stringify(manifestBody), { status: 200 }));
    expect(result.templates).toHaveLength(1);
    expect(result.templates[0]?.tier).toBe('free');
    expect(result.templates[0]?.config.rootFile).toBe('main.tex');
  });

  it('accepts multiline runtime diagnostics emitted by validated templates', async () => {
    const manifestBody = {
      version: 2,
      generatedAt: '2026-09-04T00:00:00.000Z',
      contentHash: 'validated-with-warning',
      templates: [{
        id: 'beamer-presentation', slug: 'beamer-presentation', name: 'Beamer Presentation', description: 'Slides', category: 'LaTeX', author: 'Learnitive', tier: 'free', tags: [], degreeLevels: [], requiredPackages: [], features: [],
        projectModeId: 'latex', projectType: 'document', primaryLanguageId: 'latex', primaryLanguage: 'LaTeX', useCaseId: 'presentation', validationStatus: 'valid', status: 'published', noIndex: true,
        validationRevision: 'abc123', validationTimestamp: '2026-09-04T00:00:00.000Z',
        validationError: 'first line\nsecond line',
        validationWarnings: ["Latexmk: Missing input file 'main.nav'\n  No file main.nav."],
        entryFile: 'main.tex', templateVersion: '1.0.0',
        files: [{ path: 'main.tex', url: '/api/templates/beamer-presentation/files/main.tex', size: 1 }],
      }],
    };
    const result = await loadProjectTemplateManifest(undefined, async () => new Response(JSON.stringify(manifestBody), { status: 200 }));
    expect(result.templates[0]?.validationWarnings[0]).toContain('\n');
    expect(result.templates[0]?.validationError).toBe('first line\nsecond line');
    expect(result.templates[0]?.status).toBe('published');
    expect(result.templates[0]?.noIndex).toBe(true);
  });

  it('loads managed template categories and keeps Uncategorised templates valid', async () => {
    const categories = await loadProjectTemplateCategories(undefined, async () => new Response(JSON.stringify({ categories: [
      { id: 'conference-papers', slug: 'conference-papers', name: 'Conference Papers', description: 'Proceedings', icon: 'Presentation', sortOrder: 20 },
      { id: 'research-articles', slug: 'research-articles', name: 'Research Articles', icon: 'FileSearch', sortOrder: 10 },
    ] }), { status: 200 }));
    expect(categories.map((item) => item.id)).toEqual(['research-articles', 'conference-papers']);

    const manifestBody = {
      version: 1, generatedAt: '2026-08-24T00:00:00.000Z', contentHash: 'uncategorised', templates: [{
        id: 'uncategorised', slug: 'uncategorised', name: 'Uncategorised', description: '', category: '', author: 'Learnitive', tier: 'free', tags: [], degreeLevels: [], requiredPackages: [], features: [],
        entryFile: 'main.tex', templateVersion: '1.0.0', totalBytes: 1, thumbnail: null, preview: null,
        files: [{ path: 'main.tex', url: '/api/templates/uncategorised/files/main.tex', size: 1 }],
      }],
    };
    const manifest = await loadProjectTemplateManifest(undefined, async () => new Response(JSON.stringify(manifestBody), { status: 200 }));
    expect(manifest.templates[0]?.category).toBe('');
    expect(manifest.templates[0]?.categoryId).toBeUndefined();
  });

  it('validates decoded template bytes instead of transfer Content-Length', async () => {
    const prepared = await fetchProjectTemplateFiles(template(), undefined, (async (input: RequestInfo | URL) => {
      const url = String(input);
      const value = url.endsWith('main.tex') ? 'main' : 'intro';
      return new Response(encoder.encode(value), { status: 200, headers: { 'content-length': '1' } });
    }) as typeof fetch);
    expect(prepared.map((file) => file.bytes.byteLength)).toEqual([4, 5]);
  });

  it('rejects a stale template payload before workspace creation can begin', async () => {
    await expect(fetchProjectTemplateFiles(template(), undefined, (async () => new Response(encoder.encode('changed'), { status: 200 })) as typeof fetch)).rejects.toThrow('No project was created');
  });

  it('instantiates fetched binary-safe project files', async () => {
    const storage = new MemoryProjectStorage();
    await instantiateProjectTemplate(storage, template(), undefined, fileFetcher as typeof fetch);
    expect(decoder.decode((await storage.readFile(projectFilePath('main.tex')))!)).toBe('main');
    expect(decoder.decode((await storage.readFile(projectFilePath('sections/introduction.tex')))!)).toBe('intro');
  });

  it('rolls back files and directories if template creation fails midway', async () => {
    class FailingStorage extends MemoryProjectStorage {
      override async writeFile(path: Parameters<MemoryProjectStorage['writeFile']>[0], data: Uint8Array, signal?: AbortSignal): Promise<void> {
        if (path === 'sections/introduction.tex') throw new Error('simulated template failure');
        await super.writeFile(path, data, signal);
      }
    }
    const storage = new FailingStorage();
    await expect(instantiateProjectTemplate(storage, template(), undefined, fileFetcher as typeof fetch)).rejects.toThrow('simulated template failure');
    expect(await storage.listEntries()).toEqual([]);
  });

  it('refuses to overwrite a non-empty workspace', async () => {
    const storage = new MemoryProjectStorage();
    await storage.writeFile(projectFilePath('existing.tex'), encoder.encode('keep me'));
    await expect(instantiateProjectTemplate(storage, template(), undefined, fileFetcher as typeof fetch)).rejects.toThrow('empty workspace');
    expect(decoder.decode((await storage.readFile(projectFilePath('existing.tex')))!)).toBe('keep me');
  });
});
