import { describe, it, expect } from 'vitest'
import {
  PageCustomActionSchema,
  STANDARD_CRUD_CODES,
  defaultEndpoint,
  controllerMethodNameFromEndpoint,
  serviceMethodNameFromEndpoint,
  lastPermissionSegment,
  expectedUrlPath,
  expectedControllerRoute,
  customActionsOnly,
  normalizePageCustomAction,
  type PageCustomAction,
} from '../page-spec-actions.js'

function apiAction(overrides: Partial<PageCustomAction> = {}): PageCustomAction {
  return PageCustomActionSchema.parse({
    code: 'syncFromPce',
    kind: 'api',
    scope: 'header',
    endpoint: 'sync-from-proconcept',
    httpMethod: 'POST',
    labelKey: 'list.actions.syncFromPce',
    permission: 'referentiels.types-affaire.execute',
    ucReference: 'UC-APP-REF-TYPEAFFAIRE-007',
    ...overrides,
  })
}

function navAction(overrides: Partial<PageCustomAction> = {}): PageCustomAction {
  return PageCustomActionSchema.parse({
    code: 'open',
    kind: 'navigate',
    scope: 'row',
    targetScreen: 'SCR-APP-REF-TYPEAFFAIRE-002',
    labelKey: 'list.actions.open',
    permission: 'referentiels.types-affaire.read',
    ...overrides,
  })
}

describe('lib/page-spec-actions — helpers', () => {
  it('defaultEndpoint kebabifies camelCase codes', () => {
    expect(defaultEndpoint('syncFromPce')).toBe('sync-from-pce')
    expect(defaultEndpoint('analyzeImpact')).toBe('analyze-impact')
    expect(defaultEndpoint('archive')).toBe('archive')
    expect(defaultEndpoint('mapToPce')).toBe('map-to-pce')
  })

  it('controllerMethodNameFromEndpoint returns PascalCase', () => {
    expect(controllerMethodNameFromEndpoint('sync-from-proconcept')).toBe('SyncFromProconcept')
    expect(controllerMethodNameFromEndpoint('archive')).toBe('Archive')
    expect(controllerMethodNameFromEndpoint('analyze-impact')).toBe('AnalyzeImpact')
  })

  it('serviceMethodNameFromEndpoint returns camelCase', () => {
    expect(serviceMethodNameFromEndpoint('sync-from-proconcept')).toBe('syncFromProconcept')
    expect(serviceMethodNameFromEndpoint('archive')).toBe('archive')
    expect(serviceMethodNameFromEndpoint('analyze-impact')).toBe('analyzeImpact')
  })

  it('lastPermissionSegment returns the action segment', () => {
    expect(lastPermissionSegment('referentiels.types-affaire.execute')).toBe('execute')
    expect(lastPermissionSegment('budgets.budgets.update')).toBe('update')
  })

  it('expectedUrlPath builds the right path per scope', () => {
    expect(expectedUrlPath('header', 'sync-from-pce')).toBe('/sync-from-pce')
    expect(expectedUrlPath('row', 'archive')).toBe('/{id}/archive')
    expect(expectedUrlPath('bulk', 'export-csv')).toBe('/bulk/export-csv')
  })

  it('expectedControllerRoute mirrors with {id:guid} for row scope', () => {
    expect(expectedControllerRoute('header', 'sync-from-pce')).toBe('sync-from-pce')
    expect(expectedControllerRoute('row', 'archive')).toBe('{id:guid}/archive')
    expect(expectedControllerRoute('bulk', 'export-csv')).toBe('bulk/export-csv')
  })

  it('STANDARD_CRUD_CODES excludes custom actions only', () => {
    expect(STANDARD_CRUD_CODES.has('create')).toBe(true)
    expect(STANDARD_CRUD_CODES.has('edit')).toBe(true)
    expect(STANDARD_CRUD_CODES.has('delete')).toBe(true)
    expect(STANDARD_CRUD_CODES.has('syncFromPce')).toBe(false)
    expect(STANDARD_CRUD_CODES.has('archive')).toBe(false)
  })

  it('customActionsOnly filters out CRUD codes', () => {
    const all = [
      { code: 'create' },
      { code: 'edit' },
      { code: 'syncFromPce' },
      { code: 'archive' },
    ]
    expect(customActionsOnly(all).map(a => a.code)).toEqual(['syncFromPce', 'archive'])
  })
})

describe('lib/page-spec-actions — schema validation (kind:api)', () => {
  it('accepts a minimal valid api action', () => {
    expect(() => apiAction()).not.toThrow()
  })

  it('accepts api action WITHOUT explicit endpoint (filled by normalize)', () => {
    const parsed = PageCustomActionSchema.parse({
      code: 'archive',
      kind: 'api',
      scope: 'row',
      labelKey: 'list.actions.archive',
      permission: 'budgets.budgets.update',
    })
    expect(parsed.endpoint).toBeUndefined()
    const normalized = normalizePageCustomAction(parsed)
    expect(normalized.endpoint).toBe('archive')
  })

  it('rejects code in kebab-case (must be camelCase)', () => {
    expect(() => apiAction({ code: 'sync-from-pce' as unknown as string })).toThrow(/camelCase/)
  })

  it('rejects endpoint in camelCase (must be kebab-case)', () => {
    expect(() => apiAction({ endpoint: 'syncFromPce' })).toThrow(/kebab-case/)
  })

  it('rejects malformed permission (must be 3 segments)', () => {
    expect(() => apiAction({ permission: 'flat' })).toThrow()
    expect(() => apiAction({ permission: 'two.segments' })).toThrow()
  })

  it('accepts httpMethod GET/PUT/PATCH/DELETE', () => {
    for (const m of ['GET', 'PUT', 'PATCH', 'DELETE'] as const) {
      expect(() => apiAction({ httpMethod: m })).not.toThrow()
    }
  })

  it('rejects api action carrying targetScreen', () => {
    expect(() =>
      apiAction({ targetScreen: 'SCR-APP-REF-TYPEAFFAIRE-002' }),
    ).toThrow(/api action must NOT carry targetScreen/)
  })

  it('accepts optional ucReference and guardRules', () => {
    expect(() =>
      apiAction({
        ucReference: 'UC-APP-REF-TYPEAFFAIRE-007',
        guardRules: ['BR-001', 'BR-007'],
      }),
    ).not.toThrow()
  })

  it('accepts workflowTransition shape', () => {
    expect(() =>
      apiAction({
        workflowTransition: {
          fromStatus: ['pending'],
          toStatus: 'approved',
          flowParameters: ['reason'],
        },
      }),
    ).not.toThrow()
  })
})

describe('lib/page-spec-actions — schema validation (kind:navigate)', () => {
  it('accepts a minimal navigate action with targetScreen', () => {
    expect(() => navAction()).not.toThrow()
  })

  it('accepts navigate action with targetRoute instead of targetScreen', () => {
    expect(() =>
      navAction({
        targetScreen: undefined,
        targetRoute: 'routes.referentiels.typesAffaire.detail(item.id)',
      }),
    ).not.toThrow()
  })

  it('rejects navigate action without targetScreen or targetRoute', () => {
    expect(() =>
      navAction({ targetScreen: undefined, targetRoute: undefined }),
    ).toThrow(/navigate action requires targetRoute or targetScreen/)
  })

  it('rejects navigate action carrying endpoint', () => {
    expect(() =>
      navAction({ endpoint: 'open' }),
    ).toThrow(/navigate action must NOT carry endpoint/)
  })

  it('rejects navigate action carrying payloadDto', () => {
    expect(() =>
      navAction({ payloadDto: 'OpenRequest' }),
    ).toThrow(/navigate action must NOT carry payloadDto/)
  })

  it('rejects navigate action carrying workflowTransition', () => {
    expect(() =>
      navAction({
        workflowTransition: {
          fromStatus: ['pending'],
          toStatus: 'approved',
          flowParameters: [],
        },
      }),
    ).toThrow(/navigate action must NOT carry workflowTransition/)
  })
})

describe('lib/page-spec-actions — normalizePageCustomAction', () => {
  it('fills endpoint via defaultEndpoint when missing for kind:api', () => {
    const parsed = PageCustomActionSchema.parse({
      code: 'analyzeImpact',
      kind: 'api',
      scope: 'header',
      httpMethod: 'GET',
      labelKey: 'list.actions.analyzeImpact',
      permission: 'referentiels.types-affaire.read',
    })
    expect(normalizePageCustomAction(parsed).endpoint).toBe('analyze-impact')
  })

  it('mirrors payloadType/responseType from C# names when missing', () => {
    const parsed = apiAction({ payloadDto: 'SyncRequest', responseDto: 'SyncResultDto' })
    const norm = normalizePageCustomAction(parsed)
    expect(norm.payloadType).toBe('SyncRequest')
    expect(norm.responseType).toBe('SyncResultDto')
  })

  it('returns navigate actions unchanged', () => {
    const parsed = navAction()
    expect(normalizePageCustomAction(parsed)).toEqual(parsed)
  })
})
