/**
 * P3 — Rendered-output equivalence
 *
 * Architectural invariant: the same (spec, view, model) must produce
 * semantically equivalent output in app-demo (runtime React adapter)
 * and in ReactAppStarter (Factory B emitter). They differ only in
 * execution model — one renders in the browser, the other emits
 * static JSX — but the visible structure must match.
 *
 * This file tests the LIST VIEW path, where parity is most
 * tractable because both consumers call the same Tailwind adapter
 * for the table shell. The test covers:
 *
 *   1. Column selection — both use `inferFieldsFromSchema` from
 *      the canonical pattern engine. Drift in either composer's
 *      column-filter logic would diverge here.
 *   2. Table shell HTML — the Tailwind adapter renders the same
 *      shell for both paths; ensured by Factory B importing
 *      `createUniversalTailwindAdapter` directly.
 *   3. Field humanisation — `camelCase` → `Title Case` for headers.
 *
 * Not yet covered (future extensions):
 *   - Detail / form / dashboard views — would need a reference
 *     React-adapter render path captured as an HTML string, then
 *     diffed against the Factory B emitted JSX modulo structural
 *     differences. Doable but bigger test machinery.
 *   - FK display-name resolution — blocked on Factory B's TODO to
 *     replace plain-FK inputs with dropdowns.
 */

import { describe, it, expect } from 'vitest';
import { inferFieldsFromSchema } from '@specverse/runtime/views/core';
import { createUniversalTailwindAdapter } from '@specverse/runtime/views/tailwind';
import { composeListBody } from '../list-body-composer.js';
import type { EmitContext, ModelSpec } from '../view-emitter.js';

// ───────────────────────────────────────────────────────────────────────
// Reference model + view-spec setup
// ───────────────────────────────────────────────────────────────────────

function makeReferenceModel(): ModelSpec {
  return {
    name: 'Article',
    attributes: {
      id: { type: 'UUID', required: true, auto: 'uuid4' },      // excluded
      title: { type: 'String', required: true },                // included
      slug: { type: 'String', required: true, unique: true },   // included
      content: { type: 'Text', required: false },               // included
      status: { type: 'String', values: ['draft', 'published'] },// included
      authorId: { type: 'UUID', required: true },               // included (FK)
      createdAt: { type: 'DateTime', auto: 'now' },             // excluded (auto)
      updatedAt: { type: 'DateTime', auto: 'now' },             // excluded (auto)
    },
  };
}

function makeContext(): EmitContext {
  const model = makeReferenceModel();
  return {
    view: { type: 'list', model: 'Article' },
    viewName: 'ArticleListView',
    model,
    modelSchemas: { Article: model },
    renderBody: composeListBody,
  };
}

function humanize(s: string): string {
  return s.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase()).trim();
}

// ───────────────────────────────────────────────────────────────────────
// Parity assertions
// ───────────────────────────────────────────────────────────────────────

describe('P3 — list view column inference parity', () => {
  it('runtime inferFieldsFromSchema and Factory B composer see the same columns', () => {
    const ctx = makeContext();
    const runtimeColumns = inferFieldsFromSchema(ctx.modelSchemas, ctx.model.name);

    // We can extract Factory B's column choices by looking at what
    // headers it emits into the rendered output.
    const body = composeListBody(ctx);

    for (const col of runtimeColumns) {
      const label = humanize(col);
      expect(body, `runtime column "${col}" (label: "${label}") must appear in Factory B output`)
        .toContain(`>${label}<`);
    }

    // And nothing EXTRA from the model that runtime excludes:
    const runtimeSet = new Set(runtimeColumns);
    const allAttrs = Object.keys(ctx.model.attributes);
    const runtimeExcluded = allAttrs.filter(a => !runtimeSet.has(a));
    for (const excluded of runtimeExcluded) {
      const label = humanize(excluded);
      // Factory B must NOT emit a <th> for this field.
      const headerPattern = new RegExp(`<th\\b[^>]*>${label}</th>`);
      expect(body).not.toMatch(headerPattern);
    }
  });

  it('matches the expected column set for the reference model', () => {
    const cols = inferFieldsFromSchema(makeContext().modelSchemas, 'Article');
    // Canonical expectation for this reference spec.
    expect(cols).toEqual(['title', 'slug', 'content', 'status', 'authorId']);
  });
});

describe('P3 — list view table shell parity', () => {
  it('emits a table shell byte-identical to a direct Tailwind adapter call', () => {
    // The composer calls createUniversalTailwindAdapter() + table.render()
    // with a known columns list and a sentinel in children. If we call
    // the adapter directly with the same args, we should get the same
    // shell HTML (modulo the sentinel vs the Factory B JSX injection).
    const ctx = makeContext();
    const runtimeColumns = inferFieldsFromSchema(ctx.modelSchemas, ctx.model.name);
    const headers = runtimeColumns.map(humanize);

    const adapter = createUniversalTailwindAdapter({ darkMode: true });
    const SENTINEL = 'SENTINEL';
    const shellHtml = adapter.components.table.render({
      properties: { columns: headers },
      children: SENTINEL,
    });

    // Every column header appears in the shell, in order.
    let lastIdx = -1;
    for (const header of headers) {
      const idx = shellHtml.indexOf(`>${header}</th>`);
      expect(idx, `header "${header}" must appear in Tailwind adapter output`).toBeGreaterThan(-1);
      expect(idx, `header "${header}" must appear AFTER the previous header`).toBeGreaterThan(lastIdx);
      lastIdx = idx;
    }

    // Factory B's composer output must preserve the same header order.
    const body = composeListBody(ctx);
    let lastBodyIdx = -1;
    for (const header of headers) {
      const idx = body.indexOf(`>${header}</th>`);
      expect(idx).toBeGreaterThan(-1);
      expect(idx).toBeGreaterThan(lastBodyIdx);
      lastBodyIdx = idx;
    }
  });

  it('Factory B output carries Tailwind classes from the canonical adapter shell', () => {
    const body = composeListBody(makeContext());
    // A sampling of classes that come from the adapter's table template.
    // If the adapter changes its classes, both consumers update together.
    expect(body).toContain('overflow-x-auto');
    expect(body).toContain('min-w-full');
    expect(body).toContain('divide-y divide-gray-200');
    expect(body).toContain('bg-gray-50 dark:bg-gray-700');
  });

  it('rows are synthesized by Factory B (the adapter emits placeholder for react mount)', () => {
    const body = composeListBody(makeContext());
    // The body should contain a real JSX .map() — Factory B's
    // contribution to the shared shell.
    expect(body).toContain('filtered.map((item, idx) =>');
    expect(body).toContain('onClick={() => onSelect?.(item)}');
  });
});

describe('P3 — field humanisation parity', () => {
  it('both paths humanise column names the same way (camelCase → Title Case)', () => {
    // Factory B's humanize is inlined in the composer. The runtime
    // adapter humanises via the same-shaped regex in pattern-engine /
    // react-pattern-adapter. This test documents the contract by
    // comparing against a fixture.
    const cases: [string, string][] = [
      ['title', 'Title'],
      ['authorId', 'Author Id'],
      ['createdAt', 'Created At'],
      ['slugifiedURL', 'Slugified U R L'],
      ['name', 'Name'],
    ];
    for (const [input, expected] of cases) {
      expect(humanize(input), `humanize("${input}")`).toBe(expected);
    }
  });
});
