/**
 * Wave F4 (2026-05-27) — bi-strata wire contract test.
 *
 * Proves that for every pagespec column shape supported by the bi-strata
 * stack, the C# DTO emitted by `scaffold-screen-controller` and the TS
 * interface emitted by `scaffold-api-client` (useScreens=true) describe the
 * SAME JSON wire object byte-for-byte:
 *
 *   - same property names (camelCase on JSON; PascalCase on the C# record
 *     auto-serialised to camelCase by ASP.NET's default JsonSerializer
 *     contract)
 *   - same primitive types (decimal/int/number-on-the-wire; Guid/string
 *     UUID; DateTime/ISO-8601 string; bool/boolean)
 *
 * If this test fails, frontend will crash at runtime parsing JSON that
 * doesn't match its declared interface — the silent class of bugs the
 * bi-strata refactor was meant to PREVENT, not introduce.
 *
 * The contract table below is the single source of truth — when adding a
 * new formatHint, update this table FIRST, then mirror the change in both
 * `scaffold-screen-controller/generate.ts` (`dotnetTypeFor`) and
 * `scaffold-api-client/generate.ts` (`tsTypeForColumn`).
 */
import { describe, it, expect } from 'vitest'
import { generate as generateApiClient } from '../generate.js'
import { generate as generateScreenController } from '../../../../../backend/screen-controller/cli/scaffold-screen-controller/generate.js'
import type { ScaffoldApiClientInput } from '../types.js'
import type {
  PageSpec,
  ScaffoldScreenControllerSpec,
} from '../../../../../backend/screen-controller/cli/scaffold-screen-controller/types.js'

/** Canonical contract — keep in sync with the JSDoc on
 *  `ApiScreenColumnSchema` (api-client) AND the dotnetTypeFor mapping
 *  (screen-controller). */
const CONTRACT_TABLE: Array<{
  formatHint: string
  csharpType: string
  csharpDefault: string
  tsType: string
}> = [
  { formatHint: 'currency', csharpType: 'decimal', csharpDefault: '0', tsType: 'number' },
  { formatHint: 'number', csharpType: 'decimal', csharpDefault: '0', tsType: 'number' },
  { formatHint: 'decimal', csharpType: 'decimal', csharpDefault: '0', tsType: 'number' },
  { formatHint: 'integer', csharpType: 'int', csharpDefault: '0', tsType: 'number' },
  { formatHint: 'count', csharpType: 'int', csharpDefault: '0', tsType: 'number' },
  { formatHint: 'date', csharpType: 'DateTime', csharpDefault: 'default', tsType: 'string' },
  { formatHint: 'datetime', csharpType: 'DateTime', csharpDefault: 'default', tsType: 'string' },
  { formatHint: 'bool', csharpType: 'bool', csharpDefault: 'false', tsType: 'boolean' },
  { formatHint: 'boolean', csharpType: 'bool', csharpDefault: 'false', tsType: 'boolean' },
  // guid/uuid is asymmetric: C# holds the native Guid type, but ASP.NET
  // serialises it to a string on the JSON wire (`"00000000-…"`). So the TS
  // interface declares `string`. This is the contract that matters at the
  // boundary — both ends agree on the JSON shape.
  { formatHint: 'guid', csharpType: 'Guid', csharpDefault: 'Guid.Empty', tsType: 'string' },
  { formatHint: 'uuid', csharpType: 'Guid', csharpDefault: 'Guid.Empty', tsType: 'string' },
  { formatHint: 'whatever', csharpType: 'string', csharpDefault: '""', tsType: 'string' }, // fallback
  { formatHint: '', csharpType: 'string', csharpDefault: '""', tsType: 'string' },
]

function backendSpec(overrides: Partial<ScaffoldScreenControllerSpec> = {}): ScaffoldScreenControllerSpec {
  return {
    section: 'orders',
    entity: 'Order',
    module: 'sales',
    appCode: 'erp',
    namespace: 'Erp',
    moduleDir: '/ba/ERP/SALES',
    projectPath: '/project',
    ...overrides,
  }
}

function frontendSpec(useScreens: boolean, overrides: Partial<ScaffoldApiClientInput> = {}): ScaffoldApiClientInput {
  return {
    module: 'sales',
    appCode: 'erp',
    entities: [
      {
        name: 'Order',
        section: 'orders',
        hasDashboard: false,
        fields: [
          { name: 'reference', type: 'string', required: true },
          { name: 'createdBy', type: 'string', required: true },
        ],
      },
    ],
    projectPath: '/web',
    useScreens,
    ...overrides,
  }
}

function backendPagespec(view: 'list' | 'detail', columns: PageSpec['columns']): PageSpec {
  return {
    screenCode: `SCR-ERP-SALES-ORDERS-${view.toUpperCase()}`,
    appCode: 'erp',
    module: 'sales',
    section: 'orders',
    entity: 'Order',
    view,
    permission: 'sales.orders.read',
    linkedUseCases: [],
    linkedBusinessRules: [],
    columns,
    actions: [],
  } as PageSpec
}

describe('bi-strata wire contract (Wave F4 — backend C# DTO ↔ frontend TS interface)', () => {
  for (const { formatHint, csharpType, csharpDefault, tsType } of CONTRACT_TABLE) {
    it(`formatHint="${formatHint || '(empty)'}" → C# ${csharpType} (${csharpDefault}) and TS ${tsType}`, () => {
      const column = { key: 'value', formatHint }

      // Backend
      const { files: backendFiles } = generateScreenController(backendSpec(), [
        backendPagespec('list', [column]),
      ])
      const dto = backendFiles.find(f => f.path.endsWith('OrderListScreenDto.cs'))
      expect(dto, `C# DTO must be emitted for formatHint=${formatHint}`).toBeDefined()
      // The pagespec column "value" becomes PascalCase property "Value" on
      // the C# record. Auto-serialised to lower-case "value" on the wire by
      // ASP.NET's default JsonSerializer contract — matching the TS field
      // name.
      const csharpRe = new RegExp(`public ${csharpType} Value \\{ get; init; \\} = ${csharpDefault.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')};`)
      expect(dto!.content, `Backend DTO must declare 'public ${csharpType} Value { get; init; } = ${csharpDefault};'`).toMatch(csharpRe)

      // Frontend
      const frontendFiles = generateApiClient(
        frontendSpec(true, {
          entities: [
            {
              name: 'Order',
              section: 'orders',
              hasDashboard: false,
              fields: [{ name: 'reference', type: 'string', required: true }],
              screenColumns: { list: [column] },
            },
          ],
        }),
      )
      const types = frontendFiles.find(f => f.path.endsWith('types/index.ts'))
      expect(types, `TS types file must be emitted for formatHint=${formatHint}`).toBeDefined()
      // The pagespec column key is rendered verbatim on the TS side.
      const tsRe = new RegExp(`value: ${tsType};`)
      expect(types!.content, `Frontend TS interface must declare 'value: ${tsType};'`).toMatch(tsRe)
    })
  }

  it('list+detail mismatch is caught — when detail adds columns, they appear in the TS DetailDto and the C# DetailScreenDto', () => {
    const listCols = [{ key: 'reference', formatHint: 'string' }, { key: 'status', formatHint: 'string' }]
    const detailCols = [
      ...listCols,
      { key: 'amount', formatHint: 'currency' },
      { key: 'createdAt', formatHint: 'datetime' },
    ]

    // Backend
    const { files: backendFiles } = generateScreenController(backendSpec(), [
      backendPagespec('list', listCols),
      backendPagespec('detail', detailCols),
    ])
    const detailDto = backendFiles.find(f => f.path.endsWith('OrderDetailScreenDto.cs'))!
    expect(detailDto.content).toMatch(/public string Reference \{ get; init; \}/)
    expect(detailDto.content).toMatch(/public decimal Amount \{ get; init; \}/)
    expect(detailDto.content).toMatch(/public DateTime CreatedAt \{ get; init; \}/)

    // Frontend
    const frontendFiles = generateApiClient(
      frontendSpec(true, {
        entities: [
          {
            name: 'Order',
            section: 'orders',
            hasDashboard: false,
            fields: [{ name: 'reference', type: 'string', required: true }],
            screenColumns: { list: listCols, detail: detailCols },
          },
        ],
      }),
    )
    const types = frontendFiles.find(f => f.path.endsWith('types/index.ts'))!
    expect(types.content).toMatch(/interface OrderListDto \{[\s\S]*?reference: string;[\s\S]*?status: string;[\s\S]*?\}/)
    expect(types.content).toMatch(/interface OrderDetailDto \{[\s\S]*?reference: string;[\s\S]*?amount: number;[\s\S]*?createdAt: string;[\s\S]*?\}/)
  })

  it('URL paths align — frontend GET /list matches backend [HttpGet("list")] under the same /api/screens/{plural} root', () => {
    // Backend
    const { files: backendFiles } = generateScreenController(backendSpec(), [
      backendPagespec('list', [{ key: 'reference', formatHint: 'string' }]),
    ])
    const ctrl = backendFiles.find(f => f.path.endsWith('OrdersScreenController.cs'))!
    expect(ctrl.content).toMatch(/\[Route\("api\/screens\/orders"\)\]/)
    expect(ctrl.content).toMatch(/\[HttpGet\("list"\)\]/)

    // Frontend
    const frontendFiles = generateApiClient(frontendSpec(true))
    const service = frontendFiles.find(f => f.path.endsWith('orderService.ts'))!
    expect(service.content).toMatch(/const API_PATH = '\/api\/screens\/orders';/)
    expect(service.content).toMatch(/`\$\{API_PATH\}\/list`/)
  })

  it('delete cross-stratum — frontend hits /api/v1/integration/orders/{id}, backend integration controller (not screen)', () => {
    // The screen-driven contract intentionally omits DELETE. Backend
    // scaffold-screen-controller emits no [HttpDelete] route — frontend
    // falls back to INTEGRATION_PATH.
    const { files: backendFiles } = generateScreenController(backendSpec(), [
      backendPagespec('list', [{ key: 'reference', formatHint: 'string' }]),
      backendPagespec('detail', [{ key: 'reference', formatHint: 'string' }]),
    ])
    const ctrl = backendFiles.find(f => f.path.endsWith('OrdersScreenController.cs'))!
    expect(ctrl.content).not.toMatch(/HttpDelete/)

    const frontendFiles = generateApiClient(frontendSpec(true))
    const service = frontendFiles.find(f => f.path.endsWith('orderService.ts'))!
    expect(service.content).toMatch(/const INTEGRATION_PATH = '\/api\/v1\/integration\/orders';/)
    expect(service.content).toMatch(/api\.delete\(`\$\{INTEGRATION_PATH\}\/\$\{id\}`\)/)
  })

  it('routeMode=integration — frontend hits the SAME integration route as scaffold-controller', () => {
    const frontendFiles = generateApiClient(frontendSpec(false, { routeMode: 'integration' }))
    const service = frontendFiles.find(f => f.path.endsWith('orderService.ts'))!
    expect(service.content).toMatch(/const API_PATH = '\/api\/v1\/integration\/orders';/)
  })

  it('default routeMode (direct) — frontend uses /api/{appCode}/{module}/{section}', () => {
    const frontendFiles = generateApiClient(frontendSpec(false))
    const service = frontendFiles.find(f => f.path.endsWith('orderService.ts'))!
    expect(service.content).toMatch(/const API_PATH = '\/api\/erp\/sales\/orders';/)
  })

  it('dashboard endpoints align — backend /api/screens/orders/dashboard/* matches frontend useDashboardOrder', () => {
    const { files: backendFiles } = generateScreenController(backendSpec(), [
      backendPagespec('list' as 'list', []),
    ].concat([
      { ...backendPagespec('list', []), view: 'dashboard', screenCode: 'SCR-DASH' } as PageSpec,
    ]))
    const ctrl = backendFiles.find(f => f.path.endsWith('OrdersScreenController.cs'))!
    // One endpoint per dashboard (replaces the old consolidated/alerts split).
    expect(ctrl.content).toMatch(/\[HttpGet\("dashboard"\)\]/)
    expect(ctrl.content).not.toMatch(/dashboard\/consolidated/)
    expect(ctrl.content).not.toMatch(/dashboard\/alerts/)
    // The self-contained DTO is emitted so the controller compiles standalone.
    const dto = backendFiles.find(f => f.path.endsWith('OrderDashboardDto.cs'))!
    expect(dto, 'OrderDashboardDto.cs').toBeDefined()
    expect(dto.content).toMatch(/Dictionary<string, object> Widgets/)

    // Frontend's dashboard hook uses API_PATH (= /api/screens/orders) + /dashboard.
    const frontendFiles = generateApiClient(
      frontendSpec(true, {
        entities: [
          {
            name: 'Order',
            section: 'orders',
            hasDashboard: true, // emit the dashboard method on the api-client
            fields: [{ name: 'reference', type: 'string', required: true }],
          },
        ],
      }),
    )
    const service = frontendFiles.find(f => f.path.endsWith('orderService.ts'))!
    expect(service.content).toMatch(/`\$\{API_PATH\}\/dashboard`/)
    expect(service.content).not.toMatch(/dashboard\/consolidated/)
  })
})
