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

function makeContext(overrides: Partial<EmitContext> = {}): EmitContext {
  const post: ModelSpec = {
    name: 'Post',
    attributes: {
      id: { type: 'UUID', required: true },
      title: { type: 'String', required: true },
      body: { type: 'Text', required: false },
      authorId: { type: 'UUID', required: true },
      createdAt: { type: 'DateTime', required: false },
      publishedAt: { type: 'DateTime', required: false },
    },
    relationships: {
      author: { type: 'belongsTo', targetModel: 'Author' },
    },
  };

  return {
    view: { type: 'detail', model: 'Post' },
    viewName: 'PostDetailView',
    model: post,
    modelSchemas: { Post: post },
    renderBody: composeDetailBody,
    ...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('composeDetailBody — direct output', () => {
  it('groups fields into business and belongsTo sections; omits metadata', () => {
    const body = composeDetailBody(makeContext());

    // Business fields (first <dl>)
    expect(body).toContain('>Title<');
    expect(body).toContain('>Body<');

    // belongsTo section — surfaces author as a resolved name via
    // resolveEntityDisplayName against the authorOptions array wired
    // in by view-emitter's RELATED_HOOKS substitution.
    expect(body).toContain('>Author<');
    expect(body).toContain('resolveEntityDisplayName((item as any).authorId, authorOptions)');

    // Metadata (id / createdAt / publishedAt) is deliberately NOT rendered
    // in the detail view — the list view surfaces it when users need it.
    expect(body).not.toContain('>Id<');
    expect(body).not.toContain('>Created At<');
    expect(body).not.toContain('>Published At<');
  });

  it('emits one <dt>/<dd> per business field with (item as any).FIELD access', () => {
    const body = composeDetailBody(makeContext());
    expect(body).toContain('formatDisplayValue((item as any).title)');
    expect(body).toContain('formatDisplayValue((item as any).body)');
    // Metadata access expressions are absent.
    expect(body).not.toContain('formatDisplayValue((item as any).id)');
    expect(body).not.toContain('formatDisplayValue((item as any).createdAt)');
  });

  it('falls back to a "no business fields" message when all attrs are metadata', () => {
    const mostlyMetadata: ModelSpec = {
      name: 'Stub',
      attributes: {
        id: { type: 'UUID' },
        createdAt: { type: 'DateTime' },
      },
    };
    const body = composeDetailBody(makeContext({
      model: mostlyMetadata,
      view: { type: 'detail', model: 'Stub' },
      viewName: 'StubDetailView',
      modelSchemas: { Stub: mostlyMetadata },
    }));
    expect(body).toContain('No business fields defined');
  });

  it('skips the belongsTo section when no belongsTo relationships exist', () => {
    const solo: ModelSpec = {
      name: 'Solo',
      attributes: { id: {}, name: {} },
      relationships: {
        tags: { type: 'hasMany', targetModel: 'Tag' },  // hasMany, not belongsTo
      },
    };
    const body = composeDetailBody(makeContext({
      model: solo,
      view: { type: 'detail', model: 'Solo' },
      viewName: 'SoloDetailView',
      modelSchemas: { Solo: solo },
    }));
    expect(body).not.toContain('resolveEntityDisplayName');
    expect(body).toContain('>Name<');
  });
});

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

  it('wires the skeleton: component, query hook, picker, body', () => {
    const source = emitView(makeContext());
    expect(source).toContain('export function PostDetailView');
    expect(source).toContain('usePostsQuery');
    expect(source).toContain('Select Post:');
    expect(source).toContain('formatDisplayValue((item as any).title)');
    expect(source).not.toMatch(/\{\{[A-Z_]+\}\}/);
    // Detail is inspection-only — delete/edit live in the form view.
    expect(source).not.toContain('useDeletePostMutation');
    expect(source).not.toContain('onEdit(item)');
    expect(source).not.toContain("confirm('Delete this post?')");
  });

  it('handles plural / singular correctly for loading + empty copy', () => {
    const source = emitView(makeContext());
    expect(source).toContain('Loading…');
    // SINGULAR_LOWER substitution: "No post to display."
    expect(source).toContain('No post to display');
    // PLURAL_LOWER substitution: "No posts yet" (picker empty state)
    expect(source).toContain('No posts yet');
  });
});
