import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as ts from 'typescript';
import { generate, type ExpandedSpec } from '../views-generator.js';

function makeSpec(): ExpandedSpec {
  return {
    models: {
      Post: {
        name: 'Post',
        attributes: {
          id: { type: 'UUID', required: true, auto: 'uuid4' },
          title: { type: 'String', required: true },
          body: { type: 'Text', required: false },
        },
      },
      Author: {
        name: 'Author',
        attributes: {
          id: { type: 'UUID', required: true, auto: 'uuid4' },
          name: { type: 'String', required: true },
        },
      },
    },
    views: {},
  };
}

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 as TSX:\n${message}\n\n--- source ---\n${source}`);
  }
}

describe('views-generator.generate', () => {
  it('emits 4 view files per model plus the helpers file', async () => {
    const files = await generate({ spec: makeSpec() });
    const paths = Object.keys(files);
    expect(paths).toContain('src/views/PostListView.tsx');
    expect(paths).toContain('src/views/PostDetailView.tsx');
    expect(paths).toContain('src/views/PostFormView.tsx');
    expect(paths).toContain('src/views/PostDashboardView.tsx');
    expect(paths).toContain('src/views/AuthorListView.tsx');
    expect(paths).toContain('src/views/AuthorDetailView.tsx');
    expect(paths).toContain('src/views/AuthorFormView.tsx');
    expect(paths).toContain('src/views/AuthorDashboardView.tsx');
    expect(paths).toContain('src/lib/entity-display.ts');
    expect(paths).toContain('src/hooks/useResizableSidebar.ts');
    expect(paths).toContain('src/hooks/useAppEvents.tsx');
    // 2 models × 4 view types + 3 helpers (entity-display, useResizableSidebar,
    // useAppEvents) = 11 files.
    expect(paths.length).toBe(11);
  });

  it('every emitted view file parses as valid TSX', async () => {
    const files = await generate({ spec: makeSpec() });
    for (const [path, source] of Object.entries(files)) {
      if (path.endsWith('.tsx')) assertValidTsx(source, path);
    }
  });

  it('uses a user-declared view spec in preference to a synthesized default', async () => {
    const spec: ExpandedSpec = {
      models: {
        Post: {
          name: 'Post',
          attributes: {
            id: { type: 'UUID', auto: 'uuid4' },
            title: { type: 'String', required: true },
          },
        },
      },
      views: {
        PostListView: {
          type: 'list',
          model: 'Post',
          uiComponents: {
            customTable: {
              type: 'table',
              properties: {
                columns: ['SUPER CUSTOM COLUMN'],
              },
            },
          },
        },
      },
    };
    const files = await generate({ spec });
    const out = files['src/views/PostListView.tsx'];
    // The user's custom column header is not directly visible because
    // the current composer ignores user uiComponents (it infers
    // columns from the model). But the view spec flows through — if
    // we later teach the composer to honour uiComponents, this test
    // anchors the contract.
    expect(out).toBeDefined();
    expect(out).toContain('export function PostListView');
  });

  it('warns on specialist view types and skips them', async () => {
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    try {
      const spec: ExpandedSpec = {
        models: {
          Post: { name: 'Post', attributes: { id: { auto: 'uuid4' }, title: {} } },
        },
        views: {
          PostBoardView: { type: 'board', model: 'Post' },
          PostTimelineView: { type: 'timeline', model: 'Post' },
        },
      };
      const files = await generate({ spec });
      expect(files['src/views/PostBoardView.tsx']).toBeUndefined();
      expect(files['src/views/PostTimelineView.tsx']).toBeUndefined();
      expect(warn).toHaveBeenCalled();
      const warning = warn.mock.calls[0][0] as string;
      expect(warning).toContain('Skipped');
      expect(warning).toContain('board');
      expect(warning).toContain('timeline');
    } finally {
      warn.mockRestore();
    }
  });

  it('handles an empty models map gracefully', async () => {
    const files = await generate({ spec: { models: {}, views: {} } });
    // Only the helper files are emitted; no view files.
    expect(Object.keys(files).sort()).toEqual([
      'src/hooks/useAppEvents.tsx',
      'src/hooks/useResizableSidebar.ts',
      'src/lib/entity-display.ts',
    ]);
  });
});
