import { describe, it, expect } from 'vitest'
import { generate } from '../generate.js'
import type { ScaffoldApiClientInput } from '../types.js'

function fixture(overrides: Partial<ScaffoldApiClientInput> = {}): ScaffoldApiClientInput {
  return {
    module: 'crm',
    appCode: 'TestV2',
    entities: [
      {
        name: 'Contact',
        section: 'directory',
        hasDashboard: false,
        fields: [
          { name: 'firstName', type: 'string', required: true },
          { name: 'lastName', type: 'string', required: true },
        ],
      },
    ],
    projectPath: '/web',
    ...overrides,
  }
}

describe('scaffold-api-client / generate', () => {
  it('every api.get / post call assigns result via const data = … (api already unwraps)', () => {
    const files = generate(fixture())
    const service = files.find((f) => f.path.endsWith('contactService.ts'))
    expect(service).toBeDefined()
    const lines = service!.content.split(/\r?\n/)
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]
      if (/await\s+api\.(get|post)\b/.test(line)) {
        expect(line).toMatch(/const\s+data\s*=\s*await\s+api/)
      }
    }
  })

  it('never returns the raw AxiosResponse — services return T (or void)', () => {
    const files = generate(fixture())
    const service = files.find((f) => f.path.endsWith('contactService.ts'))!
    // Forbid the raw-response anti-pattern. Hooks rely on T-shaped returns.
    expect(service.content).not.toMatch(/return\s+(?:res|response|axiosResponse)\b/)
    // Positive assertion: GET / POST return data.
    const dataReturns = service.content.match(/return\s+data\s*;/g) ?? []
    expect(dataReturns.length).toBeGreaterThanOrEqual(3) // list + detail + create
  })

  it('emits hooks under src/features/{module}/{entityLower}/hooks/', () => {
    const files = generate(fixture())
    expect(files.some((f) => f.path === 'src/features/crm/contact/hooks/useContact.ts')).toBe(true)
    expect(files.some((f) => f.path === 'src/features/crm/contact/services/contactService.ts')).toBe(true)
    expect(files.some((f) => f.path === 'src/features/crm/contact/types/index.ts')).toBe(true)
  })

  it('emits dashboard hook + service methods only when hasDashboard:true', () => {
    const noDash = generate(fixture()).find((f) => f.path.endsWith('contactService.ts'))!
    expect(noDash.content).not.toMatch(/getDashboardConsolidated/)

    const withDash = generate(
      fixture({ entities: [{ ...fixture().entities[0], hasDashboard: true, customActions: [] }] }),
    ).find((f) => f.path.endsWith('contactService.ts'))!
    expect(withDash.content).toMatch(/getDashboardConsolidated/)
    expect(withDash.content).toMatch(/getDashboardAlerts/)
  })
})

describe('scaffold-api-client / generate — custom actions (per-page mode)', () => {
  function entityWithActions(customActions: NonNullable<ScaffoldApiClientInput['entities'][0]['customActions']>) {
    return {
      name: 'Budget',
      section: 'budgets',
      hasDashboard: false,
      fields: [
        { name: 'code', type: 'string', required: true },
        { name: 'label', type: 'string', required: true },
      ],
      customActions,
    }
  }

  it('emits a service member per row-scope custom action with no payload', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [entityWithActions([
        { code: 'archive', scope: 'row', payloadType: null, responseType: 'void' },
      ])],
      projectPath: '/web',
    })
    const service = files.find((f) => f.path.endsWith('budgetService.ts'))!
    // Row-scope no-payload member: archive: async (id: string): Promise<void>
    expect(service.content).toMatch(/archive: async \(id: string\): Promise<void>/)
    expect(service.content).toMatch(/await api\.post\(`\${API_PATH}\/\${id}\/archive`\);/)
  })

  it('emits a hook per custom action using useState/isPending pattern', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [entityWithActions([
        { code: 'archive', scope: 'row', payloadType: null, responseType: 'void' },
      ])],
      projectPath: '/web',
    })
    const hook = files.find((f) => f.path.endsWith('useBudget.ts'))!
    expect(hook.content).toMatch(/export function useArchiveBudget\(\)/)
    expect(hook.content).toMatch(/isPending/)
    expect(hook.content).toMatch(/mutateAsync/)
  })

  it('emits Promise<T> return type when responseType is non-void', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [entityWithActions([
        { code: 'duplicate', scope: 'row', payloadType: null, responseType: 'BudgetDetailDto' },
      ])],
      projectPath: '/web',
    })
    const service = files.find((f) => f.path.endsWith('budgetService.ts'))!
    expect(service.content).toMatch(/duplicate: async \(id: string\): Promise<BudgetDetailDto>/)
    expect(service.content).toMatch(/const data = await api\.post<BudgetDetailDto>\(`\${API_PATH}\/\${id}\/duplicate`\);/)
    expect(service.content).toMatch(/return data;/)
  })

  it('emits row-scope action with payload as useState hook', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [entityWithActions([
        { code: 'approve', scope: 'row', payloadType: 'ApproveRequest', responseType: 'BudgetDto' },
      ])],
      projectPath: '/web',
    })
    const hook = files.find((f) => f.path.endsWith('useBudget.ts'))!
    expect(hook.content).toMatch(/export function useApproveBudget\(\)/)
    expect(hook.content).toMatch(/isPending/)
    expect(hook.content).toMatch(/mutateAsync/)
  })

  it('emits bulk-scope action with ids[] when no payloadType, posts to /bulk/<code>', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [entityWithActions([
        { code: 'archive', scope: 'bulk', payloadType: null, responseType: 'void' },
      ])],
      projectPath: '/web',
    })
    const service = files.find((f) => f.path.endsWith('budgetService.ts'))!
    expect(service.content).toMatch(/archive: async \(ids: string\[\]\)/)
    expect(service.content).toMatch(/`\${API_PATH}\/bulk\/archive`, \{ ids \}/)
    const hook = files.find((f) => f.path.endsWith('useBudget.ts'))!
    expect(hook.content).toMatch(/export function useArchiveBudget\(\)/)
  })

  it('emits header-scope action without id parameter', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [entityWithActions([
        { code: 'export', scope: 'header', payloadType: null, responseType: 'void' },
      ])],
      projectPath: '/web',
    })
    const service = files.find((f) => f.path.endsWith('budgetService.ts'))!
    expect(service.content).toMatch(/export: async \(\): Promise<void>/)
    expect(service.content).toMatch(/`\${API_PATH}\/export`/)
    const hook = files.find((f) => f.path.endsWith('useBudget.ts'))!
    expect(hook.content).toMatch(/export function useExportBudget\(\)/)
  })

  it('pascalizes kebab-case codes (`bulk-archive` → useBulkArchiveBudget)', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [entityWithActions([
        { code: 'bulk-archive', scope: 'bulk', payloadType: null, responseType: 'void' },
      ])],
      projectPath: '/web',
    })
    const hook = files.find((f) => f.path.endsWith('useBudget.ts'))!
    expect(hook.content).toMatch(/export function useBulkArchiveBudget\(\)/)
    const service = files.find((f) => f.path.endsWith('budgetService.ts'))!
    // Service member uses camelCase
    expect(service.content).toMatch(/bulkArchive: async/)
    // URL keeps kebab-case
    expect(service.content).toMatch(/`\${API_PATH}\/bulk\/bulk-archive`/)
  })

  it('emits no custom hooks/members when customActions is empty (legacy default)', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [entityWithActions([])],
      projectPath: '/web',
    })
    const service = files.find((f) => f.path.endsWith('budgetService.ts'))!
    expect(service.content).not.toMatch(/archive:/)
    expect(service.content).not.toMatch(/duplicate:/)
    const hook = files.find((f) => f.path.endsWith('useBudget.ts'))!
    expect(hook.content).not.toMatch(/useArchive/)
    expect(hook.content).not.toMatch(/useDuplicate/)
  })

  it('uses endpoint field for URL when provided, code for naming', () => {
    // Closes the historical REFERENTIELS class of 404s where the pagespec
    // `code: 'toggle-actif'` was emitted into the URL while the controller
    // route was `[HttpPost("activate")]`. With `endpoint` set, the URL
    // matches the controller; `code` continues to drive the TS identifier.
    const files = generate({
      module: 'crm',
      appCode: 'app',
      entities: [{
        name: 'Contact',
        section: 'contacts',
        hasDashboard: false,
        fields: [{ name: 'Name', type: 'string', required: true }],
        customActions: [{
          code: 'toggle-actif',
          endpoint: 'activate',
          scope: 'row',
          httpMethod: 'post',
          responseType: 'void',
        }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })

    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    // Member name derived from code (camelCase)
    expect(service.content).toMatch(/toggleActif: async \(id: string\)/)
    // URL derived from endpoint — NOT from code
    expect(service.content).toMatch(/`\$\{API_PATH\}\/\$\{id\}\/activate`/)
    expect(service.content).not.toMatch(/\/toggle-actif/)
    // Hook name still derived from code (PascalCase)
    const hook = files.find(f => f.path.endsWith('useContact.ts'))!
    expect(hook.content).toMatch(/export function useToggleActifContact\(\)/)
  })

  it('falls back to code for URL when endpoint is absent (backward compat)', () => {
    const files = generate({
      module: 'crm',
      appCode: 'app',
      entities: [{
        name: 'Contact',
        section: 'contacts',
        hasDashboard: false,
        fields: [{ name: 'Name', type: 'string', required: true }],
        customActions: [{
          code: 'archive',
          scope: 'row',
          httpMethod: 'post',
          responseType: 'void',
        }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })

    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/`\$\{API_PATH\}\/\$\{id\}\/archive`/)
  })

  it('respects httpMethod for GET actions with endpoint override', () => {
    // Mirrors the REFERENTIELS `analyze-impact` / `detect-anomalies` pattern
    // where the BA code stays verbose but the controller exposes a short
    // GET route (no body, query-only).
    const files = generate({
      module: 'crm',
      appCode: 'app',
      entities: [{
        name: 'Contact',
        section: 'contacts',
        hasDashboard: false,
        fields: [{ name: 'Name', type: 'string', required: true }],
        customActions: [{
          code: 'analyze-impact',
          endpoint: 'impact',
          scope: 'row',
          httpMethod: 'get',
          responseType: 'ImpactResult',
        }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })

    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/api\.get<ImpactResult>/)
    expect(service.content).toMatch(/`\$\{API_PATH\}\/\$\{id\}\/impact`/)
    expect(service.content).not.toMatch(/\/analyze-impact/)
  })

  it('header-scope action with endpoint override (sync-from-pce → sync-from-proconcept)', () => {
    // Closes the REFERENTIELS `sync-from-pce` → controller `sync-from-proconcept` mismatch.
    const files = generate({
      module: 'crm',
      appCode: 'app',
      entities: [{
        name: 'Employe',
        section: 'employes',
        hasDashboard: false,
        fields: [{ name: 'Name', type: 'string', required: true }],
        customActions: [{
          code: 'sync-from-pce',
          endpoint: 'sync-from-proconcept',
          scope: 'header',
          httpMethod: 'post',
          responseType: 'void',
        }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })

    const service = files.find(f => f.path.endsWith('employeService.ts'))!
    // Header-scope: no /{id}/ prefix. URL = API_PATH + endpoint.
    expect(service.content).toMatch(/`\$\{API_PATH\}\/sync-from-proconcept`/)
    expect(service.content).not.toMatch(/\/sync-from-pce/)
    // TS member name uses code → syncFromPce
    expect(service.content).toMatch(/syncFromPce: async \(\)/)
  })

  it('skips kind:"navigate" actions — no service member emitted', () => {
    // The REFERENTIELS post-mortem: BA pagespecs declared `open` actions
    // (kind:"navigate", targetScreen:SCR-…-DETAIL) which propagated through
    // ba-develop into 6 phantom `POST /{id}/open` endpoints — none of which
    // exist on the backend controllers. With the filter, scaffold-api-client
    // emits nothing for these; scaffold-component still renders the button
    // as an inline `navigate(targetRoute)`.
    const files = generate({
      module: 'crm',
      appCode: 'app',
      entities: [{
        name: 'Contact',
        section: 'contacts',
        hasDashboard: false,
        fields: [{ name: 'Name', type: 'string', required: true }],
        customActions: [
          { code: 'open', kind: 'navigate', scope: 'row', responseType: 'void' },
          // A legit API action survives the filter side-by-side.
          { code: 'archive', kind: 'api', scope: 'row', httpMethod: 'post', responseType: 'void' },
        ],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })

    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    // The navigate action MUST NOT produce a service member.
    expect(service.content).not.toMatch(/open:\s*async/)
    expect(service.content).not.toMatch(/\${API_PATH}\/\${id}\/open/)
    // The legit api action still appears.
    expect(service.content).toMatch(/archive: async \(id: string\)/)
  })

  it('skips kind:"navigate" actions — no React hook emitted', () => {
    const files = generate({
      module: 'crm',
      appCode: 'app',
      entities: [{
        name: 'Contact',
        section: 'contacts',
        hasDashboard: false,
        fields: [{ name: 'Name', type: 'string', required: true }],
        customActions: [
          { code: 'open', kind: 'navigate', scope: 'row', responseType: 'void' },
        ],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })

    const hook = files.find(f => f.path.endsWith('useContact.ts'))!
    // No `useOpenContact()` hook anywhere.
    expect(hook.content).not.toMatch(/useOpenContact/)
  })

  it('defaults kind to "api" (backward compat — actions without kind still emit)', () => {
    const files = generate({
      module: 'crm',
      appCode: 'app',
      entities: [{
        name: 'Contact',
        section: 'contacts',
        hasDashboard: false,
        fields: [{ name: 'Name', type: 'string', required: true }],
        customActions: [
          // No `kind` field — must default to 'api' per Zod schema.
          { code: 'archive', scope: 'row', responseType: 'void' },
        ],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })

    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/archive: async \(id: string\)/)
  })
})

describe('scaffold-api-client / generate — lookup hook (feeds <EntityLookup>)', () => {
  it('emits {Entity}RefDto in the types file', () => {
    const files = generate(fixture())
    const types = files.find((f) => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/export interface ContactRefDto \{\s+id: string;\s+displayName: string;\s+\}/)
  })

  it('imports {Entity}RefDto into the service', () => {
    const files = generate(fixture())
    const service = files.find((f) => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/import type \{[^}]*ContactRefDto[^}]*\} from '\.\.\/types'/)
  })

  it('emits getLookup service method hitting GET ${API_PATH}/lookup with search/page/pageSize params', () => {
    const files = generate(fixture())
    const service = files.find((f) => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/getLookup: async \(params\?: \{ search\?: string; page\?: number; pageSize\?: number \}\)/)
    expect(service.content).toMatch(/api\.get<PaginatedResult<ContactRefDto>>\(`\$\{API_PATH\}\/lookup`, \{ params \}\)/)
    expect(service.content).toMatch(/return data;/)
  })

  it('emits use{Entity}Lookup hook with useState pattern', () => {
    const files = generate(fixture())
    const hook = files.find((f) => f.path.endsWith('useContact.ts'))!
    expect(hook.content).toMatch(/export function useContactLookup\(params\?: \{ search\?: string; page\?: number; pageSize\?: number \}\)/)
    expect(hook.content).toMatch(/contactService\.getLookup\(params\)/)
    expect(hook.content).toMatch(/useState/)
  })
})

describe('scaffold-api-client / generate — Wave E (useScreens=true, screen-driven mode)', () => {
  it('sets API_PATH to /api/screens/{plural-kebab} when useScreens=true', () => {
    const files = generate(fixture({ useScreens: true }))
    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/const API_PATH = '\/api\/screens\/contacts';/)
    expect(service.content).not.toMatch(/const API_PATH = '\/api\/crm\/contacts';/)
  })

  it('declares INTEGRATION_PATH for delete + getLookup fallback', () => {
    const files = generate(fixture({ useScreens: true }))
    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/const INTEGRATION_PATH = '\/api\/v1\/integration\/contacts';/)
  })

  it('default routeMode (direct) → /api/{appCode}/{module}/{section}', () => {
    const files = generate(fixture()) // default routeMode = 'direct'
    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/const API_PATH = '\/api\/testv2\/crm\/directory';/)
    expect(service.content).not.toMatch(/INTEGRATION_PATH/)
  })

  it('routeMode integration → /api/v1/integration/{plural}', () => {
    const files = generate(fixture({ routeMode: 'integration' }))
    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/const API_PATH = '\/api\/v1\/integration\/contacts';/)
    expect(service.content).not.toMatch(/INTEGRATION_PATH/)
  })

  it('getAll hits /list and getById hits /detail/{id} in screen-driven mode', () => {
    const files = generate(fixture({ useScreens: true }))
    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/api\.get<PaginatedResult<ContactListDto>>\(`\$\{API_PATH\}\/list`/)
    expect(service.content).toMatch(/api\.get<ContactDetailDto>\(`\$\{API_PATH\}\/detail\/\$\{id\}`/)
  })

  it('create POSTs to /form and update PUTs to /form/{id} in screen-driven mode', () => {
    const files = generate(fixture({ useScreens: true }))
    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/api\.post<string>\(`\$\{API_PATH\}\/form`,/)
    expect(service.content).toMatch(/api\.put\(`\$\{API_PATH\}\/form\/\$\{id\}`,/)
  })

  it('delete falls back to INTEGRATION_PATH in screen-driven mode', () => {
    const files = generate(fixture({ useScreens: true }))
    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/api\.delete\(`\$\{INTEGRATION_PATH\}\/\$\{id\}`\)/)
  })

  it('getLookup falls back to INTEGRATION_PATH in screen-driven mode', () => {
    const files = generate(fixture({ useScreens: true }))
    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/api\.get<PaginatedResult<ContactRefDto>>\(`\$\{INTEGRATION_PATH\}\/lookup`,/)
  })
})

describe('scaffold-api-client / generate — Wave F1 (screen-driven DTOs shaped from pagespec columns)', () => {
  function entityWithScreenColumns() {
    return {
      name: 'Contact',
      section: 'directory',
      hasDashboard: false,
      fields: [
        // Random domain fields the BA does NOT want exposed on the list view.
        // Phase 3a should NOT pick the first 5 of these — it should follow
        // the pagespec columns instead.
        { name: 'firstName', type: 'string', required: true },
        { name: 'lastName', type: 'string', required: true },
        { name: 'email', type: 'string', required: false },
        { name: 'phone', type: 'string', required: false },
        { name: 'internalNotes', type: 'string', required: false },
        { name: 'secretField', type: 'string', required: false }, // never on the list
      ],
      screenColumns: {
        list: [
          { key: 'fullName', formatHint: 'string' },
          { key: 'status', formatHint: 'string' },
          { key: 'lastContactAt', formatHint: 'datetime' },
        ],
        detail: [
          { key: 'fullName', formatHint: 'string' },
          { key: 'status', formatHint: 'string' },
          { key: 'lastContactAt', formatHint: 'datetime' },
          { key: 'totalOrders', formatHint: 'integer' },
          { key: 'revenue', formatHint: 'currency' },
        ],
      },
    }
  }

  it('shapes ContactListDto from pagespec list columns (not from entity.fields)', () => {
    const files = generate(fixture({ useScreens: true, entities: [entityWithScreenColumns()] }))
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/interface ContactListDto \{[\s\S]*?fullName: string;[\s\S]*?status: string;[\s\S]*?lastContactAt: string;[\s\S]*?\}/)
    // Domain fields NOT in pagespec columns must NOT leak into the list DTO.
    expect(types.content.split('export interface ContactDetailDto')[0]).not.toMatch(/firstName:/)
    expect(types.content.split('export interface ContactDetailDto')[0]).not.toMatch(/secretField:/)
  })

  it('shapes ContactDetailDto from pagespec detail columns (richer than list)', () => {
    const files = generate(fixture({ useScreens: true, entities: [entityWithScreenColumns()] }))
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    // Detail superset: all list cols + totalOrders + revenue.
    expect(types.content).toMatch(/interface ContactDetailDto \{[\s\S]*?totalOrders: number;[\s\S]*?revenue: number;[\s\S]*?\}/)
  })

  it('maps formatHint to TS type exactly like dotnetTypeFor on the backend (parity)', () => {
    const cols = [
      { key: 'price', formatHint: 'currency' },
      { key: 'qty', formatHint: 'integer' },
      { key: 'active', formatHint: 'boolean' },
      { key: 'createdAt', formatHint: 'datetime' },
      { key: 'ownerId', formatHint: 'guid' },
      { key: 'note', formatHint: 'whatever' }, // fallback
    ]
    const ent = { ...entityWithScreenColumns(), screenColumns: { list: cols, detail: cols } }
    const files = generate(fixture({ useScreens: true, entities: [ent] }))
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/price: number;/)
    expect(types.content).toMatch(/qty: number;/)
    expect(types.content).toMatch(/active: boolean;/)
    expect(types.content).toMatch(/createdAt: string;/) // ISO-8601 on the wire
    expect(types.content).toMatch(/ownerId: string;/)
    expect(types.content).toMatch(/note: string;/) // default fallback
  })

  it('falls back to legacy entity.fields when screenColumns is omitted even if useScreens=true', () => {
    // Backward compat: a project that flips useScreens but has not yet wired
    // pagespec columns into the spec still gets a working (best-effort) TS DTO.
    const ent = {
      name: 'Contact',
      section: 'directory',
      hasDashboard: false,
      fields: [
        { name: 'firstName', type: 'string', required: true },
        { name: 'lastName', type: 'string', required: true },
      ],
    }
    const files = generate(fixture({ useScreens: true, entities: [ent] }))
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/firstName: string;/)
    expect(types.content).toMatch(/lastName: string;/)
  })

  it('uses screenColumns regardless of useScreens / routeMode (Wave F5 — gate removed)', () => {
    // Wave F5 (2026-05-27): screenColumns now win in ALL route modes.
    // Previously gated behind `isScreenMode`, which made `direct`-mode DTOs
    // fall back to entity.fields.slice(0,5) and silently mismatch the C# DTO.
    const files = generate(fixture({ useScreens: false, entities: [entityWithScreenColumns()] }))
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    // List DTO comes from pagespec columns — fullName/status/lastContactAt.
    const listSlice = types.content.split('export interface ContactDetailDto')[0]
    expect(listSlice).toMatch(/fullName: string;/)
    expect(listSlice).toMatch(/status: string;/)
    expect(listSlice).toMatch(/lastContactAt: string;/)
    // Domain fields not in pagespec MUST NOT leak into the list shape.
    expect(listSlice).not.toMatch(/firstName:/)
    expect(listSlice).not.toMatch(/secretField:/)
  })

  it('uses screenColumns in direct mode (the REFERENTIELS regression class)', () => {
    // Closes the bug where direct-mode DTOs were shaped from
    // entity.fields.slice(0,5) — wrong fields + wrong types vs the actual
    // C# controller response (Employe/Secteur/Site etc. in GAF).
    const files = generate({
      module: 'crm',
      appCode: 'app',
      entities: [{
        name: 'Contact',
        section: 'contacts',
        hasDashboard: false,
        fields: [
          // Pure domain fields — must NOT drive the wire shape any more.
          { name: 'firstName', type: 'string', required: true },
          { name: 'lastName', type: 'string', required: true },
          { name: 'internalNotes', type: 'string', required: false },
        ],
        screenColumns: {
          list: [
            { key: 'name' },
            { key: 'score', formatHint: 'number' },
            { key: 'isActive', formatHint: 'boolean' },
          ],
        },
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })

    const types = files.find(f => f.path.includes('types/index.ts'))!
    const listSlice = types.content.split('export interface ContactDetailDto')[0]
    // Shaped from screenColumns, not entity.fields
    expect(listSlice).toMatch(/name: string;/)
    expect(listSlice).toMatch(/score: number;/)
    expect(listSlice).toMatch(/isActive: boolean;/)
    expect(listSlice).not.toMatch(/firstName:/)
    expect(listSlice).not.toMatch(/internalNotes:/)
  })

  it('falls back to entity.fields.slice(0,5) when screenColumns absent in direct mode', () => {
    // Backward compatibility — any spec that hasn't been migrated to carry
    // pagespec columns still emits a well-formed DTO (best effort).
    const files = generate({
      module: 'crm',
      appCode: 'app',
      entities: [{
        name: 'Contact',
        section: 'contacts',
        hasDashboard: false,
        fields: [
          { name: 'Name', type: 'string', required: true },
          { name: 'Email', type: 'string', required: false },
          { name: 'Phone', type: 'string', required: false },
          { name: 'Score', type: 'int', required: true },
          { name: 'Active', type: 'bool', required: true },
          { name: 'Notes', type: 'string', required: false },
        ],
        // screenColumns intentionally omitted
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })

    const types = files.find(f => f.path.includes('types/index.ts'))!
    const listSlice = types.content.split('export interface ContactDetailDto')[0]
    // First 5 fields used; 6th (notes) excluded from the list shape.
    expect(listSlice).toMatch(/name: string;/)
    expect(listSlice).toMatch(/active: boolean;/)
    expect(listSlice).not.toMatch(/notes:/)
  })

  it('Create/Update Dtos still derive from entity.fields (form posts use the integration command shape)', () => {
    // The screen-driven /form endpoint reuses Create{E}Command from the
    // integration stratum (scaffold-screen-controller emits exactly that).
    // So the TS Create{E}Dto must follow entity.fields, NOT screenColumns.
    const files = generate(fixture({ useScreens: true, entities: [entityWithScreenColumns()] }))
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/interface CreateContactDto \{[\s\S]*?firstName: string;[\s\S]*?lastName: string;[\s\S]*?\}/)
    expect(types.content).toMatch(/interface UpdateContactDto \{[\s\S]*?firstName: string;[\s\S]*?email\?: string;[\s\S]*?\}/)
  })

  it('detail falls back to list columns when no detail columns are provided', () => {
    const ent = {
      ...entityWithScreenColumns(),
      screenColumns: {
        list: [{ key: 'fullName', formatHint: 'string' }, { key: 'status', formatHint: 'string' }],
        // detail omitted on purpose
      },
    }
    const files = generate(fixture({ useScreens: true, entities: [ent] }))
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/interface ContactDetailDto \{[\s\S]*?fullName: string;[\s\S]*?status: string;[\s\S]*?\}/)
  })
})

describe('scaffold-api-client / generate — Fix #1 (sub-resource parentPath)', () => {
  function subResourceFixture(overrides: Partial<ScaffoldApiClientInput> = {}): ScaffoldApiClientInput {
    return {
      module: 'referentiels',
      appCode: 'gaf',
      entities: [{
        name: 'Rue',
        pluralName: 'Rues',
        section: 'rues',
        parentPath: '/api/gaf/referentiels/sites/{parentId}/rues',
        parentIdParam: 'siteId',
        hasDashboard: false,
        fields: [{ name: 'nomRue', type: 'string', required: true }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
      ...overrides,
    } as ScaffoldApiClientInput
  }

  it('emits API_PATH as a function (parentId) => string when parentPath is set', () => {
    const files = generate(subResourceFixture())
    const service = files.find(f => f.path.endsWith('rueService.ts'))!
    expect(service.content).toMatch(/const API_PATH = \(siteId: string\) =>/)
    // Template substitution: `{parentId}` placeholder replaced by `${siteId}`
    expect(service.content).toMatch(/`\/api\/gaf\/referentiels\/sites\/\$\{siteId\}\/rues`/)
  })

  it('every CRUD service method takes siteId as its first argument', () => {
    const files = generate(subResourceFixture())
    const service = files.find(f => f.path.endsWith('rueService.ts'))!
    expect(service.content).toMatch(/getAll: async \(siteId: string, params\?:/)
    expect(service.content).toMatch(/getById: async \(siteId: string, id: string\)/)
    expect(service.content).toMatch(/create: async \(siteId: string, payload: CreateRueDto\)/)
    expect(service.content).toMatch(/update: async \(siteId: string, id: string, payload: UpdateRueDto\)/)
    expect(service.content).toMatch(/delete: async \(siteId: string, id: string\)/)
    expect(service.content).toMatch(/getLookup: async \(siteId: string, params\?:/)
  })

  it('every CRUD service method calls API_PATH(siteId) in its URL template', () => {
    const files = generate(subResourceFixture())
    const service = files.find(f => f.path.endsWith('rueService.ts'))!
    // getAll uses the function call as the URL prefix
    expect(service.content).toMatch(/api\.get<PaginatedResult<RueListDto>>\(API_PATH\(siteId\), \{ params \}\)/)
    // getById/update/delete use it inside template literals with /${id}
    expect(service.content).toMatch(/`\$\{API_PATH\(siteId\)\}\/\$\{id\}`/)
  })

  it('every hook propagates the parent ID into its signature and service call', () => {
    const files = generate(subResourceFixture())
    const hook = files.find(f => f.path.endsWith('useRue.ts'))!
    // List + detail hooks
    expect(hook.content).toMatch(/export function useRues\(siteId: string, params\?:/)
    expect(hook.content).toMatch(/export function useRue\(siteId: string, id: string\)/)
    // Mutation hooks bind the parent ID at hook-instantiation time
    expect(hook.content).toMatch(/export function useCreateRue\(siteId: string\)/)
    expect(hook.content).toMatch(/export function useUpdateRue\(siteId: string\)/)
    expect(hook.content).toMatch(/export function useDeleteRue\(siteId: string\)/)
    // Service calls thread siteId through
    expect(hook.content).toMatch(/rueService\.getAll\(siteId, params\)/)
    expect(hook.content).toMatch(/rueService\.create\(siteId, data\)/)
  })

  it('rejects parentPath + useScreens combo (mutually exclusive)', () => {
    expect(() => generate(subResourceFixture({ useScreens: true }))).toThrow(
      /parentPath.*screen mode.*mutually exclusive/i,
    )
  })

  it('non-nested entities keep their legacy signatures byte-for-byte (backward compat)', () => {
    // Default fixture is a flat Contact — no parentPath. The emitted service
    // must NOT carry any parentId parameter anywhere.
    const files = generate(fixture())
    const service = files.find(f => f.path.endsWith('contactService.ts'))!
    expect(service.content).toMatch(/const API_PATH = '/)             // plain string constant
    expect(service.content).not.toMatch(/const API_PATH = \(/)         // not a function
    expect(service.content).toMatch(/getAll: async \(params\?:/)
    expect(service.content).toMatch(/getById: async \(id: string\)/)
  })
})

describe('scaffold-api-client / generate — Wave F5 (PaginatedResult.totalCount alignment)', () => {
  it('emits totalCount (not total) in PaginatedResult — matches SmartStack.app PaginatedResult<T>.TotalCount', () => {
    const files = generate(fixture())
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/totalCount: number;/)
    // The historical `total: number` is gone — it silently dropped the count
    // for every paginated endpoint because the JSON key was always `totalCount`.
    expect(types.content).not.toMatch(/^\s*total: number;/m)
  })

  it('totalCount field is exported in all route modes (direct, integration, screens)', () => {
    for (const mode of ['direct', 'integration'] as const) {
      const f = generate(fixture({ routeMode: mode })).find(x => x.path.endsWith('types/index.ts'))!
      expect(f.content, `routeMode=${mode}`).toMatch(/totalCount: number;/)
    }
    const screens = generate(fixture({ useScreens: true })).find(x => x.path.endsWith('types/index.ts'))!
    expect(screens.content, 'useScreens=true').toMatch(/totalCount: number;/)
  })
})

describe('scaffold-api-client / generate — Bug 1 (custom action payload/response types are emitted + imported)', () => {
  // Reproducer: AFFAIRES/Demande pagespecs referenced CloseRequest, RefuseBalanceRequest,
  // ReconductRequest, ReconductResponse, PrimeApiExportFile in their customActions.
  // The CLI never declared nor imported these, so the generated <entity>Service.ts
  // failed compilation with TS2304 « cannot find name CloseRequest » 7 times.
  it('emits permissive placeholder interface when *Shape is missing', () => {
    const files = generate({
      module: 'affaires',
      appCode: 'gaf',
      entities: [{
        name: 'Demande',
        section: 'demandes',
        hasDashboard: false,
        fields: [{ name: 'Numero', type: 'string', required: true }],
        customActions: [{
          code: 'close',
          kind: 'api',
          scope: 'row',
          httpMethod: 'post',
          payloadType: 'CloseRequest',
          responseType: 'void',
        }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/export interface CloseRequest \{/)
    expect(types.content).toMatch(/\[key: string\]: unknown;/)
    expect(types.content).toMatch(/TODO\[BA-SHAPE\]/)
  })

  it('emits typed interface when payloadShape is provided', () => {
    const files = generate({
      module: 'affaires',
      appCode: 'gaf',
      entities: [{
        name: 'Demande',
        section: 'demandes',
        hasDashboard: false,
        fields: [{ name: 'Numero', type: 'string', required: true }],
        customActions: [{
          code: 'reconduct',
          kind: 'api',
          scope: 'row',
          httpMethod: 'post',
          payloadType: 'ReconductRequest',
          payloadShape: { motif: 'string', newDeadline: 'string' },
          responseType: 'ReconductResponse',
          responseShape: { newId: 'string', warnings: 'string[]' },
        }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/export interface ReconductRequest \{/)
    expect(types.content).toMatch(/motif: string;/)
    expect(types.content).toMatch(/newDeadline: string;/)
    expect(types.content).toMatch(/export interface ReconductResponse \{/)
    expect(types.content).toMatch(/newId: string;/)
    expect(types.content).toMatch(/warnings: string\[\];/)
  })

  it('imports custom payload/response types in the service file', () => {
    const files = generate({
      module: 'affaires',
      appCode: 'gaf',
      entities: [{
        name: 'Demande',
        section: 'demandes',
        hasDashboard: false,
        fields: [{ name: 'Numero', type: 'string', required: true }],
        customActions: [
          { code: 'close', scope: 'row', payloadType: 'CloseRequest', responseType: 'void' },
          { code: 'export', scope: 'header', payloadType: null, responseType: 'PrimeApiExportFile' },
        ],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const service = files.find(f => f.path.endsWith('demandeService.ts'))!
    expect(service.content).toMatch(/import type \{[^}]*CloseRequest[^}]*\} from '\.\.\/types'/)
    expect(service.content).toMatch(/import type \{[^}]*PrimeApiExportFile[^}]*\} from '\.\.\/types'/)
  })

  it('does NOT re-emit entity DTOs already exported by the file (no collision)', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [{
        name: 'Budget',
        section: 'budgets',
        hasDashboard: false,
        fields: [{ name: 'Code', type: 'string', required: true }],
        customActions: [{
          code: 'duplicate',
          scope: 'row',
          payloadType: null,
          // Reuses the entity's own DTO as a response — must not be re-declared.
          responseType: 'BudgetDetailDto',
        }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    // BudgetDetailDto exists exactly once (the entity DTO declaration).
    const matches = types.content.match(/export interface BudgetDetailDto \{/g) ?? []
    expect(matches.length).toBe(1)
  })

  it('does NOT emit declarations for TS builtins (void/string/string[])', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [{
        name: 'Budget',
        section: 'budgets',
        hasDashboard: false,
        fields: [{ name: 'Code', type: 'string', required: true }],
        customActions: [
          { code: 'archive', scope: 'row', payloadType: null, responseType: 'void' },
          { code: 'export', scope: 'header', payloadType: null, responseType: 'string' },
        ],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).not.toMatch(/export interface void/)
    expect(types.content).not.toMatch(/export interface string/)
  })
})

describe('scaffold-api-client / generate — Bug 2 (entity alias export)', () => {
  // Reproducer: scaffold-component pages emitted `import { Demande } from '.../types'`
  // but scaffold-api-client only exported `DemandeListDto` + `DemandeDetailDto`,
  // triggering 4× TS2305 « no exported member ».
  it('exports {E} as an alias of {E}DetailDto', () => {
    const files = generate(fixture())
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/export type Contact = ContactDetailDto;/)
  })

  it('alias works in every route mode (direct, integration, screens)', () => {
    for (const mode of ['direct', 'integration', 'screens'] as const) {
      const f = generate(
        mode === 'screens' ? fixture({ useScreens: true }) : fixture({ routeMode: mode }),
      ).find(x => x.path.endsWith('types/index.ts'))!
      expect(f.content, `mode=${mode}`).toMatch(/export type Contact = ContactDetailDto;/)
    }
  })
})

describe('scaffold-api-client / generate — Bug 3 (custom hook mutateAsync arg count)', () => {
  // Reproducer: `useCloseDemande` called `service.close(arg)` with a single
  // argument while `service.close(id, payload)` expected two — TS2554.
  it('row-scope + payload hook signature is mutateAsync({ id, payload })', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [{
        name: 'Budget',
        section: 'budgets',
        hasDashboard: false,
        fields: [{ name: 'Code', type: 'string', required: true }],
        customActions: [{
          code: 'approve',
          scope: 'row',
          payloadType: 'ApproveRequest',
          responseType: 'void',
        }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const hook = files.find(f => f.path.endsWith('useBudget.ts'))!
    // The hook destructures { id, payload } AND threads both into the service call.
    expect(hook.content).toMatch(/const mutateAsync = async \(\{ id, payload \}: \{ id: string; payload: ApproveRequest \}\)/)
    expect(hook.content).toMatch(/budgetService\.approve\(id, payload\)/)
    // The legacy `(idOrPayload: unknown)` / `as never` shape MUST be gone.
    expect(hook.content).not.toMatch(/idOrPayload: unknown/)
    expect(hook.content).not.toMatch(/as never/)
  })

  it('row-scope without payload keeps mutateAsync(id: string)', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [{
        name: 'Budget',
        section: 'budgets',
        hasDashboard: false,
        fields: [{ name: 'Code', type: 'string', required: true }],
        customActions: [{ code: 'archive', scope: 'row', payloadType: null, responseType: 'void' }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const hook = files.find(f => f.path.endsWith('useBudget.ts'))!
    expect(hook.content).toMatch(/const mutateAsync = async \(id: string\)/)
    expect(hook.content).toMatch(/budgetService\.archive\(id\)/)
  })

  it('bulk-scope with payload uses mutateAsync(payload)', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [{
        name: 'Budget',
        section: 'budgets',
        hasDashboard: false,
        fields: [{ name: 'Code', type: 'string', required: true }],
        customActions: [{
          code: 'bulk-archive',
          scope: 'bulk',
          payloadType: 'BulkArchiveRequest',
          responseType: 'void',
        }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const hook = files.find(f => f.path.endsWith('useBudget.ts'))!
    expect(hook.content).toMatch(/const mutateAsync = async \(payload: BulkArchiveRequest\)/)
    expect(hook.content).toMatch(/budgetService\.bulkArchive\(payload\)/)
  })

  it('header-scope without payload emits mutateAsync()', () => {
    const files = generate({
      module: 'budgets',
      appCode: 'TestV2',
      entities: [{
        name: 'Budget',
        section: 'budgets',
        hasDashboard: false,
        fields: [{ name: 'Code', type: 'string', required: true }],
        customActions: [{ code: 'export', scope: 'header', payloadType: null, responseType: 'void' }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const hook = files.find(f => f.path.endsWith('useBudget.ts'))!
    expect(hook.content).toMatch(/const mutateAsync = async \(\): Promise<void>/)
    expect(hook.content).toMatch(/budgetService\.export\(\)/)
  })

  it('header-scope + payload + parent threads parentId into service call', () => {
    const files = generate({
      module: 'referentiels',
      appCode: 'gaf',
      entities: [{
        name: 'Rue',
        section: 'rues',
        parentPath: '/api/gaf/referentiels/sites/{parentId}/rues',
        parentIdParam: 'siteId',
        hasDashboard: false,
        fields: [{ name: 'NomRue', type: 'string', required: true }],
        customActions: [{
          code: 'sync',
          scope: 'header',
          payloadType: 'SyncRequest',
          responseType: 'void',
        }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const hook = files.find(f => f.path.endsWith('useRue.ts'))!
    // Hook signature carries siteId; mutateAsync takes only the payload; call
    // threads both: service.sync(siteId, payload).
    expect(hook.content).toMatch(/export function useSyncRue\(siteId: string\)/)
    expect(hook.content).toMatch(/const mutateAsync = async \(payload: SyncRequest\)/)
    expect(hook.content).toMatch(/rueService\.sync\(siteId, payload\)/)
  })
})

describe('scaffold-api-client / generate — Bug 8 (DetailDto = union(entity.fields, screenColumns.detail))', () => {
  // Reproducer: AFFAIRES/Demande pagespec declared screenColumns.detail without
  // localityData / addressData / parcelData (JSON-snapshot nvarchar(max) fields
  // present in entity.fields). The edit form fetched DemandeDetailDto then tried
  // to pre-fill these fields — TypeScript reported "property does not exist on
  // type DemandeDetailDto" because the DTO had no clue about them.
  it('DetailDto includes entity.fields not present in screenColumns.detail', () => {
    const files = generate({
      module: 'affaires',
      appCode: 'gaf',
      entities: [{
        name: 'Demande',
        section: 'demandes',
        hasDashboard: false,
        fields: [
          { name: 'Numero', type: 'string', required: true },
          { name: 'LocalityData', type: 'string', required: false },   // JSON snapshot, missing from detail
          { name: 'AddressData', type: 'string', required: false },
          { name: 'ParcelData', type: 'string', required: false },
        ],
        screenColumns: {
          list: [{ key: 'numero', formatHint: 'string' }],
          // Only Numero in detail — the 3 JSON fields are intentionally absent.
          detail: [{ key: 'numero', formatHint: 'string' }],
        },
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    // DetailDto contains every entity field, regardless of whether the BA listed it.
    expect(types.content).toMatch(/export interface DemandeDetailDto \{[\s\S]*?numero:[\s\S]*?localityData\?: string;[\s\S]*?addressData\?: string;[\s\S]*?parcelData\?: string;/)
  })

  it('DetailDto preserves screenColumns.detail order before entity.fields extras', () => {
    const files = generate({
      module: 'affaires',
      appCode: 'gaf',
      entities: [{
        name: 'Demande',
        section: 'demandes',
        hasDashboard: false,
        fields: [
          { name: 'Numero', type: 'string', required: true },
          { name: 'Snapshot', type: 'string', required: false },
        ],
        screenColumns: {
          detail: [
            { key: 'numero', formatHint: 'string' },
            { key: 'displayedStatus', formatHint: 'string' },  // computed display field
          ],
        },
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    // displayedStatus (column-only) appears BEFORE snapshot (entity-only).
    const detailMatch = types.content.match(/export interface DemandeDetailDto \{([\s\S]*?)\n\}/)
    expect(detailMatch).toBeTruthy()
    const body = detailMatch![1]
    const numeroIdx = body.indexOf('numero:')
    const displayedIdx = body.indexOf('displayedStatus:')
    const snapshotIdx = body.indexOf('snapshot?:')
    expect(numeroIdx).toBeGreaterThan(-1)
    expect(displayedIdx).toBeGreaterThan(numeroIdx)
    expect(snapshotIdx).toBeGreaterThan(displayedIdx)
  })

  it('legacy fallback (no screenColumns) keeps entity.fields shape byte-for-byte', () => {
    const files = generate(fixture())
    const types = files.find(f => f.path.endsWith('types/index.ts'))!
    // Contact fixture has firstName + lastName in entity.fields, no screenColumns.
    expect(types.content).toMatch(/export interface ContactDetailDto \{[\s\S]*?firstName: string;[\s\S]*?lastName: string;/)
  })
})

describe('scaffold-api-client / generate — Bug 5 (lookup hook contract is locked)', () => {
  it('always exports use{E}Lookup at hooks/use{E}.ts', () => {
    const files = generate(fixture())
    const hook = files.find(f => f.path === 'src/features/crm/contact/hooks/useContact.ts')
    expect(hook).toBeDefined()
    expect(hook!.content).toMatch(/export function useContactLookup/)
  })

  it('lookup hook is emitted in every route mode (direct, integration, screens)', () => {
    for (const mode of ['direct', 'integration'] as const) {
      const f = generate(fixture({ routeMode: mode })).find(x => x.path.endsWith('useContact.ts'))!
      expect(f.content, `routeMode=${mode}`).toMatch(/export function useContactLookup/)
    }
    const screens = generate(fixture({ useScreens: true })).find(x => x.path.endsWith('useContact.ts'))!
    expect(screens.content, 'useScreens=true').toMatch(/export function useContactLookup/)
  })

  it('lookup hook is emitted even when the entity has a parentPath (sub-resource)', () => {
    const files = generate({
      module: 'referentiels',
      appCode: 'gaf',
      entities: [{
        name: 'Rue',
        section: 'rues',
        parentPath: '/api/gaf/referentiels/sites/{parentId}/rues',
        parentIdParam: 'siteId',
        hasDashboard: false,
        fields: [{ name: 'NomRue', type: 'string', required: true }],
      }],
      projectPath: '/test',
      routeMode: 'direct',
    })
    const hook = files.find(f => f.path.endsWith('useRue.ts'))!
    expect(hook.content).toMatch(/export function useRueLookup\(siteId: string,/)
  })
})
