import { describe, it, expect } from 'vitest';
import * as ts from 'typescript';
import { composeFormBody } from '../form-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, auto: 'uuid4' },
      title: { type: 'String', required: true },
      body: { type: 'Text', required: false },
      published: { type: 'Boolean', required: false },
      score: { type: 'Integer', required: false },
      email: { type: 'Email', required: false },
      status: { type: 'String', required: false, values: ['draft', 'live', 'archived'] },
      authorId: { type: 'UUID', required: true },   // belongsTo FK
      externalId: { type: 'UUID', required: false },   // non-FK UUID
      createdAt: { type: 'DateTime', required: false },   // metadata
      metadata: { type: 'Json', required: false },   // #43C — Json type
    },
    relationships: {
      author: { type: 'belongsTo', targetModel: 'Author' },
    },
  };

  return {
    view: { type: 'form', model: 'Post' },
    viewName: 'PostFormView',
    model: post,
    modelSchemas: { Post: post },
    renderBody: composeFormBody,
    ...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('composeFormBody — field selection', () => {
  it('omits auto-generated fields (id, createdAt) and attrs with auto markers', () => {
    const body = composeFormBody(makeContext());
    expect(body).not.toMatch(/htmlFor="id"/);
    expect(body).not.toMatch(/htmlFor="createdAt"/);
  });

  it('includes business fields in order', () => {
    const body = composeFormBody(makeContext());
    expect(body).toContain('htmlFor="title"');
    expect(body).toContain('htmlFor="body"');
    expect(body).toContain('htmlFor="score"');
  });

  it('renders a message when there are no editable fields', () => {
    const emptyish: ModelSpec = {
      name: 'Bare',
      attributes: {
        id: { type: 'UUID', auto: 'uuid4' },
        createdAt: { type: 'DateTime' },
      },
    };
    const body = composeFormBody(makeContext({
      model: emptyish,
      view: { type: 'form', model: 'Bare' },
      viewName: 'BareFormView',
      modelSchemas: { Bare: emptyish },
    }));
    expect(body).toContain('No editable fields for this model.');
  });
});

describe('composeFormBody — input type per attribute', () => {
  it('String with values=[...] → select', () => {
    const body = composeFormBody(makeContext());
    expect(body).toMatch(/<select\s[^>]*id="status"/);
    expect(body).toContain('<option value="draft">draft</option>');
    expect(body).toContain('<option value="live">live</option>');
  });

  it('Text → textarea', () => {
    const body = composeFormBody(makeContext());
    expect(body).toMatch(/<textarea\s[^>]*id="body"/);
  });

  it('Boolean → checkbox', () => {
    const body = composeFormBody(makeContext());
    expect(body).toContain('type="checkbox"');
    expect(body).toContain('id="published"');
    expect(body).toContain("checked={Boolean((formData as any).published)}");
  });

  it('Integer → number input with Number() coercion', () => {
    const body = composeFormBody(makeContext());
    expect(body).toMatch(/id="score"[\s\S]*type="number"/);
    expect(body).toContain("Number(e.target.value)");
  });

  it('Email → email input', () => {
    const body = composeFormBody(makeContext());
    expect(body).toMatch(/id="email"[\s\S]*type="email"/);
  });

  it('String default → text input', () => {
    const body = composeFormBody(makeContext());
    expect(body).toMatch(/id="title"[\s\S]*type="text"/);
  });

  it('UUID → text input (treated like string)', () => {
    // externalId is a plain UUID attribute — no belongsTo relationship
    // shadows it, so it renders as a text input. (FK UUIDs like
    // authorId render as a <select> instead; see the belongsTo suite.)
    const body = composeFormBody(makeContext());
    expect(body).toMatch(/id="externalId"[\s\S]*type="text"/);
  });

  it('Json → textarea using formatJsonForInput + parseJsonInput helpers (#43C)', () => {
    // Pre-#43C, a Json field fell through to the default <input type="text">
    // branch — single-line, no parse handling, mangled nested structure
    // on round-trip. The new branch renders <textarea> and delegates
    // read/write formatting to runtime helpers (formatJsonForInput,
    // parseJsonInput from @specverse/runtime/views/core/entity-display)
    // so the emitted JSX stays clean (no inline IIFE that confuses the
    // parser) and the parsing rules live in one tested place.
    const body = composeFormBody(makeContext());
    expect(body).toMatch(/<textarea\s[^>]*id="metadata"/);
    expect(body).not.toMatch(/id="metadata"[\s\S]{0,80}?type="text"/);
    expect(body).toContain('formatJsonForInput((formData as any).metadata)');
    expect(body).toContain("handleChange('metadata', parseJsonInput(e.target.value))");
  });
});

describe('composeFormBody — required markers', () => {
  it('adds required HTML attribute to required inputs; omits it on optional ones', () => {
    const body = composeFormBody(makeContext());
    // The bareword ` required` attribute, emitted at end of an onChange
    // line. Required fields in the fixture: title, authorId (belongsTo FK
    // emitted as required). Optional: body, published, score, email, status.
    const occurrences = (body.match(/ required$/gm) ?? []).length;
    expect(occurrences).toBe(2);
  });

  it("appends ' *' to the label of required fields", () => {
    const body = composeFormBody(makeContext());
    expect(body).toContain('Title *');
    // body is not required
    expect(body).not.toContain('Body *');
  });
});

describe('composeFormBody — belongsTo section', () => {
  it('emits a <select> for each belongsTo, iterating ${relName}Options', () => {
    const body = composeFormBody(makeContext());
    // Author belongsTo User — author is the relationship name, so the
    // options array is authorOptions and the FK column is authorId.
    expect(body).toMatch(/<select[\s\S]*?id="authorId"/);
    expect(body).toContain('authorOptions.map');
    expect(body).toContain('getEntityDisplayName(opt)');
  });

  it('does not emit plain text inputs for FK columns shadowed by a belongsTo', () => {
    // authorId is shadowed by the author belongsTo relationship — it
    // should only render once (as the <select>), never as a text input.
    const body = composeFormBody(makeContext());
    expect(body).not.toMatch(/<input[\s\S]*?id="authorId"[\s\S]*?type="text"/);
  });
});

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

  it('wires the skeleton: component, hooks, modes, body', () => {
    const source = emitView(makeContext());
    expect(source).toContain('export function PostFormView');
    expect(source).toContain('useCreatePostMutation');
    expect(source).toContain('useUpdatePostMutation');
    expect(source).toContain('mode === \'create\'');
    expect(source).toContain('Create Post');
    expect(source).toContain('Update Post');
    // Body injected
    expect(source).toContain('htmlFor="title"');
    expect(source).toContain('<textarea');
    expect(source).not.toMatch(/\{\{[A-Z_]+\}\}/);
  });
});
