/**
 * Anti-lexicon guard (BLUEPRINT-V2.md Δ22).
 *
 * Two sweeps, both case- and accent-insensitive (NFD normalization):
 *   1. Every UI-visible VALUE exported by app/lexicon.ts — strings, Record
 *      values, array items, and the return of template functions called with
 *      dummy params. Export NAMES and Record KEYS are code identifiers and
 *      are NOT tested (e.g. REVIEW_VERDICTS is a legal identifier even
 *      though « verdict » is forbidden in UI text).
 *   2. The <template> section of every MARKED .vue file under app/.
 *      A file is scanned only when it carries the magic marker comment
 *      <!-- ld-v2 -->. EVERY v2 screen MUST carry this marker (the screen
 *      agents add it); unmarked v1 leftovers are ignored on purpose — they
 *      are scheduled for replacement, not for compliance.
 */
import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import * as lexicon from '../app/lexicon'
import { FORBIDDEN_UI_TERMS } from '../app/lexicon'

const APP_DIR = fileURLToPath(new URL('../app', import.meta.url))

/** Lowercase + strip diacritics: « Délégué » → « delegue ». */
function normalize(text: string): string {
  return text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
}

const NORMALIZED_TERMS = FORBIDDEN_UI_TERMS.map(term => ({ term, needle: normalize(term) }))

/** Forbidden terms contained in a text, after normalization. */
function forbiddenTermsIn(text: string): string[] {
  const haystack = normalize(text)
  return NORMALIZED_TERMS.filter(({ needle }) => haystack.includes(needle)).map(({ term }) => term)
}

interface Violation {
  source: string // export path or file path
  term: string
  excerpt: string
}

/** Call a template function with dummy params (numbers interpolate cleanly
 *  into template literals AND support toLocaleString; fall back to neutral
 *  strings for functions that require them). */
function callTemplateFn(fn: (...args: never[]) => unknown): unknown {
  const arity = Math.max(fn.length, 1)
  try {
    return fn(...(Array.from({ length: arity }, (_, i) => i + 1) as never[]))
  } catch {
    return fn(...(Array.from({ length: arity }, () => 'exemple') as never[]))
  }
}

/** Recursively collect forbidden-term violations over exported VALUES only. */
function walkValue(value: unknown, path: string, violations: Violation[]): void {
  if (typeof value === 'string') {
    for (const term of forbiddenTermsIn(value)) {
      violations.push({ source: path, term, excerpt: value })
    }
    return
  }
  if (Array.isArray(value)) {
    value.forEach((item, i) => walkValue(item, `${path}[${i}]`, violations))
    return
  }
  if (typeof value === 'function') {
    walkValue(callTemplateFn(value as (...args: never[]) => unknown), `${path}(…)`, violations)
    return
  }
  if (value !== null && typeof value === 'object') {
    // Record: VALUES only — keys are code identifiers.
    for (const [key, item] of Object.entries(value)) {
      walkValue(item, `${path}.${key}`, violations)
    }
  }
  // numbers / booleans / null / undefined: nothing to check
}

/** Recursive .vue listing under dir. */
function listVueFiles(dir: string): string[] {
  const files: string[] = []
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const full = join(dir, entry.name)
    if (entry.isDirectory()) files.push(...listVueFiles(full))
    else if (entry.isFile() && entry.name.endsWith('.vue')) files.push(full)
  }
  return files
}

/**
 * Scan the <template> section of every .vue file under `dir` that carries
 * the magic marker comment (default <!-- ld-v2 -->). The whole template is
 * scanned on purpose — the banned vocabulary must appear NOWHERE in it,
 * bound attributes included. Every v2 screen MUST carry the marker.
 */
export function scanVueTemplates(dir: string, marker = 'ld-v2'): Violation[] {
  const markerRe = new RegExp(`<!--\\s*${marker}\\s*-->`)
  const violations: Violation[] = []
  for (const file of listVueFiles(dir)) {
    const content = readFileSync(file, 'utf-8')
    if (!markerRe.test(content)) continue
    const start = content.indexOf('<template')
    const end = content.lastIndexOf('</template>')
    if (start === -1 || end === -1) continue
    const template = content.slice(start, end + '</template>'.length)
    for (const term of forbiddenTermsIn(template)) {
      violations.push({ source: file, term, excerpt: `terme présent dans <template>` })
    }
  }
  return violations
}

function formatReport(violations: Violation[]): string {
  return violations
    .map(v => `${v.source} → terme interdit « ${v.term} » : ${v.excerpt}`)
    .join('\n')
}

describe('anti-lexique — lexicon.ts', () => {
  it('exports at least the known label tables (sanity check of the walk)', () => {
    expect(Object.keys(lexicon).length).toBeGreaterThan(10)
    expect(typeof lexicon.thresholdSentence).toBe('function')
  })

  it('no exported UI value contains a forbidden term', () => {
    const violations: Violation[] = []
    for (const [name, value] of Object.entries(lexicon)) {
      if (name === 'FORBIDDEN_UI_TERMS') continue // the ban list itself
      walkValue(value, name, violations)
    }
    expect(formatReport(violations)).toBe('')
  })
})

describe('anti-lexique — marked .vue templates (<!-- ld-v2 -->)', () => {
  it('no marked template contains a forbidden term', () => {
    const violations = scanVueTemplates(APP_DIR)
    expect(formatReport(violations)).toBe('')
  })
})
