// Tests for the Fehlerreport-Regel-Engine (R1-R6), KAR-889 / P0.4.
//
// Fixtures are entirely synthetic (invented field values/names) — no real
// Brose/Kiekert/Autoliv data or files. Field NAMES referenced from
// 03-fehlerreport-analyse.md §2/§5 are reused (they are BMW template labels, // allow-customer-string
// not customer content); the underlying numbers are made up.

import { describe, it, expect } from 'vitest'
import type { QAFRow } from '@/lib/qaf-parser'
import type { QafSummary, QafSummaryKey } from '../types'
import {
  classifyFieldState,
  evaluateRuleEngine,
  ruleViolationToPlausibilityIssue,
  canonicalizeFieldLabel,
  disambiguateLabels,
  blockedStepFields,
  RULE_ENGINE_CONFIG,
  type RuleEngineInput,
} from '../rule-engine'

function summary(values: Partial<Record<QafSummaryKey, string | null>>): QafSummary {
  const keys: QafSummaryKey[] = [
    'partNumber',
    'quotationDate',
    'supplier',
    'partName',
    'variant',
    'project',
    'requestVersion',
    'changeIndex',
    'supplierNo',
    // KAR-910
    'peakVolumeYear',
    'productionStartSop',
    'deliverySite',
    'shiftsPerWeek',
    // QVS-P4
    'plannedCapacity',
    'lotSize',
  ]
  const out = {} as QafSummary
  for (const k of keys) out[k] = { value: values[k] ?? null, cell: null }
  return out
}

function mkRow(overrides: Partial<QAFRow> = {}): QAFRow {
  return {
    positionsnummer: '1',
    teilebenennung: 'Teil',
    prozessbezeichnung: 'Montage',
    bezeichnungAnlage: 'Anlage',
    standort: 'Werk',
    beschaffungswaehrung: 'EUR',
    angebotswaehrung: 'EUR',
    zykluszeit: 10,
    teileProZyklus: 1,
    anzahlMA: 1,
    lohnkosten: 30,
    lohnzuschlagssaetze: 10,
    mss: 50,
    ruestkosten: 0,
    fek: 0,
    rfgk: 0,
    fk: 100,
    wechselkurs: 1,
    anzahlProAngebotsteil: 1,
    fkAW: 100,
    ausschuss: 2,
    ausschusskosten: 1,
    ...overrides,
  }
}

function fileInput(overrides: { summary?: Partial<Record<QafSummaryKey, string | null>>; steps?: QAFRow[] } = {}) {
  return {
    summary: summary({ partNumber: '9990001', ...overrides.summary }),
    steps: overrides.steps ?? [mkRow()],
  }
}

const cleanInput: RuleEngineInput = { alt: fileInput(), neu: fileInput() }

describe('classifyFieldState', () => {
  it('numeric value present -> provided_valid', () => {
    expect(classifyFieldState({ value: 42, mandatory: true })).toBe('provided_valid')
  })

  it('text value present -> provided_valid', () => {
    expect(classifyFieldState({ value: 'Montage', mandatory: true })).toBe('provided_valid')
  })

  it('blank text, mandatory -> empty_comparison_critical', () => {
    expect(classifyFieldState({ value: '', mandatory: true })).toBe('empty_comparison_critical')
  })

  it('blank text, not mandatory -> empty_allowed', () => {
    expect(classifyFieldState({ value: '', mandatory: false })).toBe('empty_allowed')
  })

  it('numeric blank with no rawText, mandatory -> empty_comparison_critical', () => {
    expect(classifyFieldState({ value: null, mandatory: true })).toBe('empty_comparison_critical')
  })

  it('numeric blank with no rawText, not mandatory -> empty_allowed', () => {
    expect(classifyFieldState({ value: null, mandatory: false })).toBe('empty_allowed')
  })

  it('numeric blank with n.a. rawText -> not_applicable, regardless of mandatory', () => {
    expect(classifyFieldState({ value: null, rawText: 'n.a.', mandatory: true })).toBe('not_applicable')
    expect(classifyFieldState({ value: null, rawText: 'entfällt', mandatory: false })).toBe('not_applicable')
  })

  it('numeric blank with unrecognized garbled rawText -> provided_invalid', () => {
    expect(classifyFieldState({ value: null, rawText: 'siehe Anlage 3', mandatory: false })).toBe('provided_invalid')
    expect(classifyFieldState({ value: null, rawText: 'tbd', mandatory: true })).toBe('provided_invalid')
  })
})

describe('evaluateRuleEngine — R2 Pflichtfeld-Blockade (mandatory field empty/invalid)', () => {
  it('clean input (all mandatory fields filled) -> no R2/R3 violations', () => {
    const violations = evaluateRuleEngine(cleanInput)
    expect(violations.filter((v) => v.ruleId === 'R2')).toHaveLength(0)
    expect(violations.filter((v) => v.ruleId === 'R3')).toHaveLength(0)
  })

  it('mandatory step field blank (Positionsnummer) -> R2, severity kritisch, warn by default', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ positionsnummer: '' })] }),
    }
    const violations = evaluateRuleEngine(input)
    const r2 = violations.find((v) => v.ruleId === 'R2' && v.field?.includes('Positionsnummer'))
    expect(r2).toBeDefined()
    expect(r2?.severity).toBe('kritisch')
    expect(r2?.enforcement).toBe('warn')
    expect(r2?.comparisonBlocked).toBe(false)
  })

  it('mandatory summary field blank (BMW Sachnummer) -> R2', () => { // allow-customer-string
    const input: RuleEngineInput = {
      alt: fileInput({ summary: { partNumber: null } }),
      neu: fileInput(),
    }
    const violations = evaluateRuleEngine(input)
    const r2 = violations.find((v) => v.ruleId === 'R2' && v.field?.includes('Sachnummer'))
    expect(r2).toBeDefined()
    expect(r2?.severity).toBe('kritisch')
  })

  it('mandatory numeric field (FK [AW]) with garbled text content -> R2 provided_invalid', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ fkAW: null, rawText: { fkAW: 'siehe Nachtrag' } })] }),
    }
    const violations = evaluateRuleEngine(input)
    const r2 = violations.find((v) => v.ruleId === 'R2' && v.field?.includes('FK [AW]'))
    expect(r2).toBeDefined()
  })

  it('block config -> R2 sets comparisonBlocked=true and enforcement=block', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ angebotswaehrung: '' })] }),
    }
    const violations = evaluateRuleEngine(input, { ruleEnforcement: 'block' })
    const r2 = violations.find((v) => v.ruleId === 'R2')
    expect(r2?.enforcement).toBe('block')
    expect(r2?.comparisonBlocked).toBe(true)
  })

  it('mandatory field explicitly marked n.a. -> no violation (not_applicable is a legitimate fill, not an error)', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ fkAW: null, rawText: { fkAW: 'n.a.' } })] }),
    }
    const violations = evaluateRuleEngine(input)
    expect(violations.filter((v) => v.ruleId === 'R2')).toHaveLength(0)
  })

  it('default RULE_ENGINE_CONFIG is warn (weich starten)', () => {
    expect(RULE_ENGINE_CONFIG.ruleEnforcement).toBe('warn')
  })
})

// Reviewer finding (KAR-889 adversarial review): the block-mode switch has to
// actually gate the specific FieldDiff shown in the A1-Delta-Tabelle, not just
// tag the issue_type. That requires the engine to expose WHICH row/field is
// blocked, not just that a violation exists — rowIndex/fieldKey + the
// blockedStepFields() lookup below is what compare.ts uses for that.
describe('evaluateRuleEngine — R2 exposes rowIndex/fieldKey for step-level violations', () => {
  it('a mandatory step-field violation carries the row index (position in .steps) and the raw field key', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow(), mkRow({ fkAW: null })] }),
    }
    const violations = evaluateRuleEngine(input, { ruleEnforcement: 'block' })
    const r2 = violations.find((v) => v.ruleId === 'R2' && v.fieldKey === 'fkAW')
    expect(r2?.rowIndex).toBe(1)
    expect(r2?.side).toBe('NEU')
  })

  it('summary-level R2 violations carry no rowIndex/fieldKey (not step-scoped)', () => {
    const input: RuleEngineInput = { alt: fileInput({ summary: { partNumber: null } }), neu: fileInput() }
    const violations = evaluateRuleEngine(input)
    const r2 = violations.find((v) => v.ruleId === 'R2' && v.section === 'summary')
    expect(r2?.rowIndex).toBeUndefined()
    expect(r2?.fieldKey).toBeUndefined()
  })
})

describe('blockedStepFields — lookup consumed by compare.ts to gate FieldDiffs', () => {
  it('block mode: returns the blocked (rowIndex -> fieldKey set) per side', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow(), mkRow({ positionsnummer: '' })] }),
    }
    const violations = evaluateRuleEngine(input, { ruleEnforcement: 'block' })
    const blocked = blockedStepFields(violations)
    expect(blocked.NEU.get(1)?.has('positionsnummer')).toBe(true)
    expect(blocked.NEU.get(0)).toBeUndefined()
    expect(blocked.ALT.size).toBe(0)
  })

  it('warn mode: nothing is blocked even though the violation exists', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ positionsnummer: '' })] }),
    }
    const violations = evaluateRuleEngine(input) // default warn
    const blocked = blockedStepFields(violations)
    expect(blocked.ALT.size).toBe(0)
    expect(blocked.NEU.size).toBe(0)
  })

  it('R3 (optional, never blocking) never contributes to the blocked lookup, even under block config', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ ausschusskosten: null, rawText: { ausschusskosten: 'noch offen' } })] }),
    }
    const violations = evaluateRuleEngine(input, { ruleEnforcement: 'block' })
    const blocked = blockedStepFields(violations)
    expect(blocked.NEU.get(0)?.has('ausschusskosten')).toBeUndefined()
  })
})

describe('evaluateRuleEngine — R3 Nicht-Pflichtfeld inkorrekt (optional field invalid)', () => {
  it('optional numeric field blank (no rawText) -> no violation at all (legitimately unused)', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ ausschusskosten: null })] }),
    }
    const violations = evaluateRuleEngine(input)
    expect(violations.filter((v) => v.field?.includes('Ausschusskosten'))).toHaveLength(0)
  })

  it('optional numeric field with garbled non-n.a. text -> R3, severity hinweis, never blocking', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ ausschusskosten: null, rawText: { ausschusskosten: 'noch offen' } })] }),
    }
    const violations = evaluateRuleEngine(input)
    const r3 = violations.find((v) => v.ruleId === 'R3')
    expect(r3).toBeDefined()
    expect(r3?.severity).toBe('hinweis')
    expect(r3?.enforcement).toBe('warn')
    expect(r3?.comparisonBlocked).toBe(false)
  })

  it('R3 stays warn even under block config (open Fachfrage 6.1, hard-wired non-blocking)', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ ausschusskosten: null, rawText: { ausschusskosten: 'noch offen' } })] }),
    }
    const violations = evaluateRuleEngine(input, { ruleEnforcement: 'block' })
    const r3 = violations.find((v) => v.ruleId === 'R3')
    expect(r3?.enforcement).toBe('warn')
    expect(r3?.comparisonBlocked).toBe(false)
  })

  it('optional numeric field with n.a. marker -> no violation', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ ausschusskosten: null, rawText: { ausschusskosten: 'entfällt' } })] }),
    }
    const violations = evaluateRuleEngine(input)
    expect(violations.filter((v) => v.field?.includes('Ausschusskosten'))).toHaveLength(0)
  })
})

describe('evaluateRuleEngine — R4 Sektion nur in einer Version (struktureller Unterschied, kein Fehler)', () => {
  it('NEU has steps, ALT has none -> R4 hinweis, non-blocking, no error severity', () => {
    const input: RuleEngineInput = {
      alt: fileInput({ steps: [] }),
      neu: fileInput({ steps: [mkRow()] }),
    }
    const violations = evaluateRuleEngine(input)
    const r4 = violations.find((v) => v.ruleId === 'R4')
    expect(r4).toBeDefined()
    expect(r4?.severity).toBe('hinweis')
    expect(r4?.comparisonBlocked).toBe(false)
    expect(r4?.side).toBe('ALT')
  })

  it('ALT has steps, NEU has none -> R4 for NEU side', () => {
    const input: RuleEngineInput = {
      alt: fileInput({ steps: [mkRow()] }),
      neu: fileInput({ steps: [] }),
    }
    const violations = evaluateRuleEngine(input)
    const r4 = violations.find((v) => v.ruleId === 'R4')
    expect(r4?.side).toBe('NEU')
  })

  it('both sides have steps -> no R4', () => {
    const violations = evaluateRuleEngine(cleanInput)
    expect(violations.filter((v) => v.ruleId === 'R4')).toHaveLength(0)
  })

  it('both sides empty -> no R4 (nothing to compare, not a one-sided difference)', () => {
    const input: RuleEngineInput = { alt: fileInput({ steps: [] }), neu: fileInput({ steps: [] }) }
    const violations = evaluateRuleEngine(input)
    expect(violations.filter((v) => v.ruleId === 'R4')).toHaveLength(0)
  })
})

describe('R6 — Label-Robustheit (Tippfehler/Duplikate), fehlerreport-analyse §4.2', () => {
  it('canonicalizes documented BMW-Fehlerreport typos to their correct label', () => { // allow-customer-string
    expect(canonicalizeFieldLabel('Teilebennung')).toBe('teilebenennung')
    expect(canonicalizeFieldLabel('Produdktionsstart')).toBe('produktionsstart')
    expect(canonicalizeFieldLabel('Verrechungsform')).toBe('verrechnungsform')
  })

  it('passes through unknown labels unchanged (lowercased/trimmed)', () => {
    expect(canonicalizeFieldLabel('  Bauteilbezeichnung ')).toBe('bauteilbezeichnung')
  })

  it('disambiguates duplicate labels within a section via position suffix (Mengeneinheit 2x in MATERIAL)', () => {
    const keys = disambiguateLabels('MATERIAL', ['Mengeneinheit', 'Angebotswährung', 'Mengeneinheit'])
    expect(keys).toEqual(['MATERIAL:mengeneinheit', 'MATERIAL:angebotswährung', 'MATERIAL:mengeneinheit#2'])
  })

  it('same label in different sections is not conflated', () => {
    const a = disambiguateLabels('MATERIAL', ['Mengeneinheit'])
    const b = disambiguateLabels('SBM-DEVICES-FWZ', ['Mengeneinheit'])
    expect(a[0]).not.toBe(b[0])
  })
})

describe('ruleViolationToPlausibilityIssue', () => {
  it('maps a warn-mode R2 violation onto the existing PlausibilityIssue shape', () => {
    const input: RuleEngineInput = { alt: fileInput(), neu: fileInput({ steps: [mkRow({ positionsnummer: '' })] }) }
    const [v] = evaluateRuleEngine(input).filter((x) => x.ruleId === 'R2')
    const issue = ruleViolationToPlausibilityIssue(v)
    expect(issue.severity).toBe('kritisch')
    expect(issue.type).toBe('rule_r2_mandatory_warn')
    expect(issue.explanation).toBe(v.messageDe)
  })

  it('maps a block-mode R2 violation with a distinguishable issue_type (no schema change needed)', () => {
    const input: RuleEngineInput = { alt: fileInput(), neu: fileInput({ steps: [mkRow({ positionsnummer: '' })] }) }
    const [v] = evaluateRuleEngine(input, { ruleEnforcement: 'block' }).filter((x) => x.ruleId === 'R2')
    const issue = ruleViolationToPlausibilityIssue(v)
    expect(issue.type).toBe('rule_r2_mandatory_blocked')
  })

  it('maps R3 with issue_type rule_r3_optional_invalid', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ ausschusskosten: null, rawText: { ausschusskosten: 'noch offen' } })] }),
    }
    const [v] = evaluateRuleEngine(input).filter((x) => x.ruleId === 'R3')
    expect(ruleViolationToPlausibilityIssue(v).type).toBe('rule_r3_optional_invalid')
  })

  it('maps R4 with issue_type rule_r4_structural_diff', () => {
    const input: RuleEngineInput = { alt: fileInput({ steps: [] }), neu: fileInput({ steps: [mkRow()] }) }
    const [v] = evaluateRuleEngine(input).filter((x) => x.ruleId === 'R4')
    expect(ruleViolationToPlausibilityIssue(v).type).toBe('rule_r4_structural_diff')
  })
})

// KAR-906/P3.2: R1-R6 already carry messageDe+messageEn (KAR-889) — this item
// verifies the persistence bridge stops dropping messageEn (previously only
// `explanation` — DE — reached PlausibilityIssue, per the module header's
// documented KAR-889 "not persisted today" gap).
describe('ruleViolationToPlausibilityIssue — bilingual (KAR-906)', () => {
  it('R2 warn: explanationEn matches the violation messageEn, distinct from DE', () => {
    const input: RuleEngineInput = { alt: fileInput(), neu: fileInput({ steps: [mkRow({ positionsnummer: '' })] }) }
    const [v] = evaluateRuleEngine(input).filter((x) => x.ruleId === 'R2')
    const issue = ruleViolationToPlausibilityIssue(v)
    expect(issue.explanationEn).toBe(v.messageEn)
    expect(issue.explanationEn).not.toBe(issue.explanation)
    expect(issue.explanationEn).toMatch(/Required field/)
  })

  it('R2 block: explanationEn is carried too', () => {
    const input: RuleEngineInput = { alt: fileInput(), neu: fileInput({ steps: [mkRow({ positionsnummer: '' })] }) }
    const [v] = evaluateRuleEngine(input, { ruleEnforcement: 'block' }).filter((x) => x.ruleId === 'R2')
    expect(ruleViolationToPlausibilityIssue(v).explanationEn).toBe(v.messageEn)
  })

  it('R3: explanationEn is carried', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ ausschusskosten: null, rawText: { ausschusskosten: 'noch offen' } })] }),
    }
    const [v] = evaluateRuleEngine(input).filter((x) => x.ruleId === 'R3')
    const issue = ruleViolationToPlausibilityIssue(v)
    expect(issue.explanationEn).toBe(v.messageEn)
    expect(issue.explanationEn).toMatch(/not filled in correctly/)
  })

  it('R4: explanationEn is carried', () => {
    const input: RuleEngineInput = { alt: fileInput({ steps: [] }), neu: fileInput({ steps: [mkRow()] }) }
    const [v] = evaluateRuleEngine(input).filter((x) => x.ruleId === 'R4')
    const issue = ruleViolationToPlausibilityIssue(v)
    expect(issue.explanationEn).toBe(v.messageEn)
    expect(issue.explanationEn).toMatch(/structural difference/)
  })
})

// #284 adversarial-review fix (confidence 88): buildViolation used to
// interpolate the SAME German fieldLabel into messageDe AND messageEn
// (STEP_FIELD_LABELS/SUMMARY_FIELD_LABELS are DE-only literal maps) — an
// EN-locale reader would have seen sentences like `Required field "BMW // allow-customer-string
// Sachnummer" is not filled in correctly...`. These assert the REAL
// canonical labelEn text (canonical-fields.ts), not just "some EN text
// exists" — mirrors the reconciliation.ts/business-rules.ts field-name
// wiring this PR already did for every other namespace.
describe('R2/R3 field names use the canonical labelEn, not the German label (KAR-906, #284 fix)', () => {
  it('R2 on a mandatory STEP field (Positionsnummer) uses "Item number Manufacturing step" in EN, not the DE label', () => {
    const input: RuleEngineInput = { alt: fileInput(), neu: fileInput({ steps: [mkRow({ positionsnummer: '' })] }) }
    const [v] = evaluateRuleEngine(input).filter((x) => x.ruleId === 'R2')
    expect(v.field).toBe('Positionsnummer Fertigungsschritt')
    expect(v.messageDe).toContain('Positionsnummer Fertigungsschritt')
    expect(v.messageEn).toContain('Item number Manufacturing step')
    expect(v.messageEn).not.toContain('Positionsnummer')
  })

  it('R2 on the mandatory SUMMARY field (BMW Sachnummer) uses "BMW part number" in EN, not the DE label', () => { // allow-customer-string
    const input: RuleEngineInput = { alt: fileInput({ summary: { partNumber: '' } }), neu: fileInput() }
    const [v] = evaluateRuleEngine(input).filter((x) => x.ruleId === 'R2' && x.section === 'summary')
    expect(v.field).toBe('BMW Sachnummer') // allow-customer-string
    expect(v.messageDe).toContain('BMW Sachnummer') // allow-customer-string
    expect(v.messageEn).toContain('BMW part number') // allow-customer-string
    expect(v.messageEn).not.toContain('Sachnummer') // allow-customer-string
  })

  it('R3 on an optional STEP field (Ausschusskosten Fertigung) uses "Scrap costs Manufacturing [AW]" in EN, not the DE label', () => {
    const input: RuleEngineInput = {
      alt: fileInput(),
      neu: fileInput({ steps: [mkRow({ ausschusskosten: null, rawText: { ausschusskosten: 'noch offen' } })] }),
    }
    const [v] = evaluateRuleEngine(input).filter((x) => x.ruleId === 'R3')
    expect(v.field).toBe('Ausschusskosten Fertigung [AW]')
    expect(v.messageDe).toContain('Ausschusskosten Fertigung [AW]')
    expect(v.messageEn).toContain('Scrap costs Manufacturing [AW]')
    expect(v.messageEn).not.toContain('Ausschusskosten')
  })
})

// ── 15 synthetische Feld-Level-Regressionsfälle (fehlerreport-analyse.md §5,
// Zeilen #6,#10,#12,#14,#16,#23,#36,#42,#67,#71,#74,#75,#92,#94,#100). Feldnamen
// aus der Analyse übernommen (BMW-Template-Labels, kein Kundeninhalt), alle // allow-customer-string
// Werte erfunden. SUMMARY-Felder (#6,#10,#12,#14,#16,#23) laufen über
// evaluateRuleEngine mit echten QafSummary/QAFRow-Fixtures; MATERIAL/SBM-Felder
// (#36,#42,#67,#71,#74,#75,#92,#94,#100) haben in Kadi-v2 noch keinen Parser
// (P1.6/P1.7 offen) — sie demonstrieren dieselbe R1-Klassifikationsregel direkt
// über classifyFieldState (generisch, feldnamen-unabhängig).
describe('15 synthetische Regressionsfälle aus fehlerreport-analyse.md §5', () => {
  it('#6 Anfragenummer/Version — mandatory=false, leer -> kein Fehler (empty_allowed)', () => {
    expect(classifyFieldState({ value: '', mandatory: false })).toBe('empty_allowed')
  })

  it('#10 Peakvolumen — mandatory=false, unparseable Inhalt -> provided_invalid (R3-Kandidat)', () => {
    expect(classifyFieldState({ value: null, rawText: 'wird nachgereicht', mandatory: false })).toBe(
      'provided_invalid',
    )
  })

  it('#12 Produktionsstart (SOP) — mandatory=false, gültiges Datum als Text -> provided_valid', () => {
    expect(classifyFieldState({ value: '2027-03-01', mandatory: false })).toBe('provided_valid')
  })

  it('#14 Auslieferungsstandort — mandatory=false, gefüllt -> provided_valid', () => {
    expect(classifyFieldState({ value: 'Werk Süd', mandatory: false })).toBe('provided_valid')
  })

  it('#16 Schichten — mandatory=false, numerisch gefüllt -> provided_valid', () => {
    expect(classifyFieldState({ value: 3, mandatory: false })).toBe('provided_valid')
  })

  it('#23 Cost Breakdown AW1 — mandatory=true, negativ/unparseable Text -> provided_invalid (R2-Kandidat)', () => {
    expect(classifyFieldState({ value: null, rawText: 'negativ, siehe Kommentar', mandatory: true })).toBe(
      'provided_invalid',
    )
  })

  it('#36 MATERIAL Mengeneinheit (Label-Duplikat) — mandatory=true, leer -> empty_comparison_critical', () => {
    expect(classifyFieldState({ value: null, mandatory: true })).toBe('empty_comparison_critical')
  })

  it('#42 MATERIAL Verpackungskosten (Angebot) — mandatory=true, gefüllt -> provided_valid', () => {
    expect(classifyFieldState({ value: 12.5, mandatory: true })).toBe('provided_valid')
  })

  it('#67 SBM Bauteilbezeichnung — mandatory=true, leer -> empty_comparison_critical', () => {
    expect(classifyFieldState({ value: '', mandatory: true })).toBe('empty_comparison_critical')
  })

  it('#71 SBM Blechdicke [mm] — mandatory=true, unparseable Text -> provided_invalid', () => {
    expect(classifyFieldState({ value: null, rawText: 'entfällt bei diesem Werkzeug', mandatory: true })).toBe(
      'provided_invalid',
    )
  })

  it('#74 SBM Anzahl Stufen/Takte: Aktiv — mandatory=true, gefüllt -> provided_valid', () => {
    expect(classifyFieldState({ value: 2, mandatory: true })).toBe('provided_valid')
  })

  it('#75 SBM Anzahl Stufen/Takte: Leer — mandatory=true, n.a. -> not_applicable (kein Fehler)', () => {
    expect(classifyFieldState({ value: null, rawText: 'n.a.', mandatory: true })).toBe('not_applicable')
  })

  it('#92 SBM Mech. Elemente gesamt: Schieber gesamt — mandatory=true, leer -> empty_comparison_critical', () => {
    expect(classifyFieldState({ value: null, mandatory: true })).toBe('empty_comparison_critical')
  })

  it('#94 SBM Beschaffungswährung [BW] — mandatory=true, gefüllt -> provided_valid', () => {
    expect(classifyFieldState({ value: 'EUR', mandatory: true })).toBe('provided_valid')
  })

  it('#100 SBM Summe Werkzeugkosten SBM [AW] — mandatory=true, leer -> empty_comparison_critical', () => {
    expect(classifyFieldState({ value: null, mandatory: true })).toBe('empty_comparison_critical')
  })
})
