import { describe, it, expect } from 'vitest';
import * as ts from 'typescript';
import { generate as generateAppTsx } from '../app-tsx-generator.js';
import { generate as generatePackageJson } from '../package-json-generator.js';

function assertValidTsx(source: string, label: string): void {
  const result = ts.transpileModule(source, {
    compilerOptions: {
      jsx: ts.JsxEmit.Preserve,
      target: ts.ScriptTarget.ES2022,
      module: ts.ModuleKind.ESNext,
      moduleResolution: ts.ModuleResolutionKind.Bundler,
      strict: false,
    },
    reportDiagnostics: true,
  });
  const errors = result.diagnostics?.filter(d => d.category === ts.DiagnosticCategory.Error) ?? [];
  if (errors.length > 0) {
    const message = errors
      .map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'))
      .join('\n');
    throw new Error(`${label} failed to parse:\n${message}\n\n--- source ---\n${source}`);
  }
}

describe('app-tsx-generator', () => {
  it('produces valid TSX with one branch per model and view type', async () => {
    const source = await generateAppTsx({
      spec: {
        models: {
          Post: { name: 'Post', attributes: {} },
          Author: { name: 'Author', attributes: {} },
        },
      },
    });
    assertValidTsx(source, 'App.tsx');
    // One nav section per model
    expect(source).toContain('Post');
    expect(source).toContain('Author');
    // One import per (model, view) pair — 2 models × 4 view types = 8 imports
    const importLines = source.match(/^import \{ \w+View \} from '\.\/views\//gm) ?? [];
    expect(importLines.length).toBe(8);
    // Navigation wiring — post-R0, setSelection is inlined (no select() helper).
    // Selection state is a discriminated union with kind='model' for CRUD views.
    expect(source).toContain(`setSelection({ kind: 'model', model: 'Post', view: 'list' })`);
    expect(source).toContain(`setSelection({ kind: 'model', model: 'Author', view: 'dashboard' })`);
  });

  it('handles an empty spec gracefully', async () => {
    const source = await generateAppTsx({ spec: { models: {}, views: {} } });
    assertValidTsx(source, 'App.tsx (empty spec)');
    expect(source).toContain('No models or operations');
    // Still renders a QueryClientProvider
    expect(source).toContain('QueryClientProvider');
  });

  it('uses @tanstack/react-query, not @specverse/runtime', async () => {
    const source = await generateAppTsx({
      spec: { models: { Post: { name: 'Post', attributes: {} } } },
    });
    expect(source).not.toContain('@specverse/runtime');
    expect(source).toContain('@tanstack/react-query');
  });
});

describe('package-json-generator', () => {
  it('omits @specverse/runtime from dependencies', async () => {
    const json = await generatePackageJson({ spec: { name: 'test' } });
    const pkg = JSON.parse(json);
    const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
    expect(allDeps['@specverse/runtime']).toBeUndefined();
    expect(allDeps['@specverse/engines']).toBeUndefined();
    expect(allDeps['@specverse/types']).toBeUndefined();
  });

  it('includes the deps the emitted views actually need', async () => {
    const json = await generatePackageJson({ spec: { name: 'test' } });
    const pkg = JSON.parse(json);
    expect(pkg.dependencies).toMatchObject({
      react: expect.any(String),
      'react-dom': expect.any(String),
      '@tanstack/react-query': expect.any(String),
    });
    expect(pkg.devDependencies).toMatchObject({
      typescript: expect.any(String),
      vite: expect.any(String),
      tailwindcss: expect.any(String),
    });
  });

  it('slugifies the app name', async () => {
    const json = await generatePackageJson({
      spec: { metadata: { name: 'My Fancy App!' } },
    });
    const pkg = JSON.parse(json);
    expect(pkg.name).toBe('my-fancy-app');
  });

  it('falls back to a default name when none is provided', async () => {
    const json = await generatePackageJson({ spec: {} });
    const pkg = JSON.parse(json);
    expect(pkg.name).toBe('specverse-starter-app');
  });

  it('parses as valid JSON with the expected shape', async () => {
    const json = await generatePackageJson({ spec: { name: 'test' } });
    const pkg = JSON.parse(json);
    expect(pkg.type).toBe('module');
    expect(pkg.scripts).toMatchObject({
      dev: 'vite',
      build: 'tsc && vite build',
      preview: 'vite preview',
    });
  });
});
