import { describe, it, expect } from 'vitest';
import * as ts from 'typescript';
import { composeListBody } from '../list-body-composer.js';
import { emitView, type EmitContext, type ModelSpec } from '../view-emitter.js';

/**
 * End-to-end: the canonical Tailwind adapter renders the table shell,
 * the composer injects a JSX row-map, the emitter substitutes into
 * the skeleton. The full output must parse as valid TSX.
 */

function makeContext(overrides: Partial<EmitContext> = {}): EmitContext {
  const post: ModelSpec = {
    name: 'Post',
    attributes: {
      // `id` is excluded by runtime's inferFieldsFromSchema directly.
      id: { type: 'UUID', required: true },
      title: { type: 'String', required: true },
      body: { type: 'Text', required: false },
      // `auto=now` is what marks publishedAt as excluded from column
      // inference — the canonical rule from runtime pattern-engine.
      publishedAt: { type: 'DateTime', required: false, auto: 'now' },
      authorId: { type: 'UUID', required: true },
    },
  };

  return {
    view: { type: 'list', model: 'Post' },
    viewName: 'PostListView',
    model: post,
    modelSchemas: { Post: post },
    renderBody: composeListBody,
    ...overrides,
  };
}

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('composeListBody — direct output', () => {
  it('produces JSX containing the canonical table shell', () => {
    const body = composeListBody(makeContext());
    // Comes from the Tailwind adapter's table template
    expect(body).toContain('overflow-x-auto');
    expect(body).toContain('min-w-full');
    expect(body).toContain('<thead');
    expect(body).toContain('<tbody');
    // After html-to-jsx: class → className
    expect(body).toContain('className=');
    expect(body).not.toMatch(/\bclass=/);
  });

  it('renders one header cell per non-metadata attribute, humanized', () => {
    const body = composeListBody(makeContext());
    expect(body).toContain('>Title<');
    expect(body).toContain('>Body<');
    expect(body).toContain('>Author Id<');   // authorId humanized
    expect(body).not.toContain('>Id<');       // id filtered as metadata
    expect(body).not.toContain('>Published At<'); // publishedAt filtered as metadata
  });

  it('emits a filtered.map expression with one cell per column', () => {
    const body = composeListBody(makeContext());
    expect(body).toContain('filtered.map((item, idx) =>');
    expect(body).toContain('onClick={() => onSelect?.(item)}');

    // Count how many times each field name appears in cell-context (inside
    // the `<td>...formatDisplayValue((item as any).FIELD)...` substring).
    // Metadata should appear 0 times; business fields exactly once each.
    const cellsRegion = body.slice(body.indexOf('<tbody'));
    const cellAppearances = (field: string) =>
      (cellsRegion.match(new RegExp(`formatDisplayValue\\(\\(item as any\\)\\.${field}\\)`, 'g')) ?? []).length;

    expect(cellAppearances('title')).toBe(1);
    expect(cellAppearances('body')).toBe(1);
    expect(cellAppearances('authorId')).toBe(1);
    expect(cellAppearances('id')).toBe(0);           // metadata
    expect(cellAppearances('publishedAt')).toBe(0);  // metadata
  });

  it('handles a model with zero non-metadata attributes', () => {
    const empty: ModelSpec = {
      name: 'Pulse',
      attributes: { id: { type: 'UUID', required: true } },
    };
    const body = composeListBody(makeContext({
      model: empty,
      view: { type: 'list', model: 'Pulse' },
      viewName: 'PulseListView',
      modelSchemas: { Pulse: empty },
    }));
    // Table renders with empty columns array (no <th> cells inside <tr>)
    expect(body).toContain('<thead');
    // Row map still present — but cells block empty
    expect(body).toContain('filtered.map');
  });
});

describe('emitView wired to composeListBody — end-to-end', () => {
  it('produces a complete PostListView.tsx that parses', () => {
    const source = emitView(makeContext());
    assertValidTsx(source, 'PostListView');
  });

  it('integrates skeleton + composer correctly', () => {
    const source = emitView(makeContext());
    // From skeleton
    expect(source).toContain('export function PostListView');
    expect(source).toContain('usePostsQuery');
    expect(source).toContain('useDeletePostMutation');
    // From composer (canonical adapter shell + row map)
    expect(source).toContain('filtered.map');
    expect(source).toContain('onSelect?.(item)');
    expect(source).toContain('>Title<');
    // No stray sentinels or placeholders
    expect(source).not.toContain('__SPECVERSE_TBODY_ROWS__');
    expect(source).not.toMatch(/\{\{[A-Z_]+\}\}/);
  });

  it('produces readable, line-broken output (no wall of HTML)', () => {
    const source = emitView(makeContext());
    // Sanity: the emitted file should be multi-line idiomatic React,
    // not one giant collapsed line.
    const lines = source.split('\n');
    expect(lines.length).toBeGreaterThan(30);
    // And no obviously-HTML escape leakage
    expect(source).not.toContain('dangerouslySetInnerHTML');
  });
});
