// Tests for the Business-Rule-Reconciliation-Engine (KAR-901 / P2.2):
// row-level, independent recomputation of Leitfaden formulas — distinct from
// reconciliation.ts (KAR-887/P0.2), which only checks AGGREGATES (Summary vs.
// Zeilen-Summe). This module checks the individual row itself.
//
// Fixtures are entirely synthetic (invented field values) — no real
// Brose/Kiekert/Autoliv data or files.

import { describe, it, expect } from 'vitest'
import type { QAFRow, QAFRowValues } from '@/lib/qaf-parser'
import type { MaterialRow, MaterialRowValues } from '../material-parser'
import type { SbmRow, SbmRowValues } from '../sbm-parser'
import {
  evaluateBusinessRules,
  checkBusinessRules,
  businessRuleResultToPlausibilityIssue,
  BUSINESS_RULES_CONFIG,
  type BusinessRuleInput,
  type BusinessRuleResult,
} from '../business-rules'

function mkRow(o: Partial<QAFRow> = {}): QAFRow {
  const base: QAFRowValues = {
    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: null,
    rfgk: 0,
    fk: null,
    wechselkurs: 1,
    anzahlProAngebotsteil: 1,
    fkAW: 100,
    ausschuss: 2,
    ausschusskosten: null,
  }
  return { ...base, ...o }
}

function mkMaterialRow(o: Partial<MaterialRowValues> = {}): MaterialRow {
  const base: MaterialRowValues = {
    positionNumber: '1',
    partDesignation: 'Teil',
    materialDesignation: 'Kunststoffgehaeuse',
    supplier: 'Testlieferant',
    technicalFunction: '',
    countryOfOrigin: 'DE',
    htsCode: '',
    unitOfMeasure: 'Unit_(purchased_part)',
    procurementCurrency: 'EUR',
    costPerUnitBw: 1,
    quotationCurrency: 'EUR',
    exchangeRate: 1,
    packagingCostPerUnit: 0,
    transportCostPerUnit: 0,
    customsCbamCostPerUnit: 0,
    overheadCost: 0,
    costPerUnitAw: null,
    referenceQuantity: null,
    netQuantity: null,
    rebate: null,
    quantityPerQuotedPart: 1,
    materialCost: null,
    scrapRate: 0,
    scrapCost: null,
    rawMaterialDesignation: '',
    referenceWeight: null,
    rawMaterialQuotation: null,
    rawMaterialSurcharge: null,
    packagingCostOffer: null, // KAR-910
  }
  return { ...base, ...o, sourceCells: {}, normalized: {}, rawText: (o as MaterialRow).rawText ?? {} }
}

function mkSbmRow(o: Partial<SbmRowValues> = {}): SbmRow {
  const base: SbmRowValues = {
    positionNumber: '1',
    verrechnungsform: 'SBM',
    toolFixtureType: 'Spritzgießwerkzeuge',
    componentDesignation: 'Teil',
    cavityConfiguration: '',
    stageCount: null,
    stageCountActive: null, // KAR-910
    stageCountEmpty: null, // KAR-910
    serviceLifeCycles: null,
    toolDeploymentDate: '',
    orderLeadTimeWeeks: null,
    manufacturerLocation: '',
    procurementCurrency: 'EUR',
    toolFixtureCostBw: 100,
    bmwPartNumber: '',
    componentIndex: '',
    length: null,
    width: null,
    height: null,
    sheetThickness: null,
    componentSurface: '',
    toolDimensions: '',
    toolWeight: null,
    toolConcept: '',
    connectionConcept: '',
    slidersGeneral: null,
    slidersHydraulic: null,
    slidersAngled: null,
    slidersJaw: null,
    slidersTotal: null,
    remarks: '',
    quotationCurrency: 'EUR',
    exchangeRate: 1,
    toolFixtureCount: 1,
    totalToolFixtureCostAw: null,
    totalSbmToolCostAw: null,
    devicesFollowupMaintenancePerUnit: null,
  }
  return {
    ...base,
    ...o,
    sourceCells: {},
    normalized: {},
    rawText: (o as SbmRow).rawText ?? {},
    deviceClassification: (o as SbmRow).deviceClassification ?? null,
  }
}

function find(results: BusinessRuleResult[], checkId: string, rowIndex = 0): BusinessRuleResult | undefined {
  return results.find((r) => r.checkId === checkId && r.rowIndex === rowIndex)
}

// ── MANUFACTURING: rule_calc_fek ────────────────────────────────────────────

describe('evaluateBusinessRules — rule_calc_fek (FEK[BW], Leitfaden S.30)', () => {
  it('matching FEK -> bestanden, no issue', () => {
    // FEK = [1 * 30 * (1+0.10) + 50] * 10/1/3600 + 0 = (33+50)*10/3600 = 0.230555...
    const expected = (1 * 30 * (1 + 10 / 100) + 50) * (10 / 1) / 3600 + 0
    const input: BusinessRuleInput = { side: 'ALT', steps: [mkRow({ fek: expected })] }
    const results = evaluateBusinessRules(input)
    const r = find(results, 'rule_calc_fek')
    expect(r?.status).toBe('bestanden')
    expect(checkBusinessRules(input).some((i) => i.type === 'rule_calc_fek')).toBe(false)
  })

  it('a deviating FEK produces an abweichung with concrete numbers', () => {
    const input: BusinessRuleInput = { side: 'NEU', steps: [mkRow({ fek: 999 })] }
    const results = evaluateBusinessRules(input)
    const r = find(results, 'rule_calc_fek')
    expect(r?.status).toBe('abweichung')
    expect(r?.actual).toBe(999)
    expect(r?.expected).toBeCloseTo((1 * 30 * 1.1 + 50) * (10 / 3600), 6)
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_fek')
    expect(issue).toBeDefined()
    expect(issue?.severity).toBe('pruefen')
    expect(issue?.explanation).toContain('999')
  })

  it('no fek value on the row (blank/not applicable to this row) -> no result at all (skip, not nicht_pruefbar)', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [mkRow({ fek: null })] }
    const results = evaluateBusinessRules(input)
    expect(find(results, 'rule_calc_fek')).toBeUndefined()
  })

  it('fek present but a required input (lohnkosten) missing -> nicht_pruefbar with reason', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [mkRow({ fek: 1, lohnkosten: null })] }
    const results = evaluateBusinessRules(input)
    const r = find(results, 'rule_calc_fek')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toContain('Lohnkosten')
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_fek_nicht_pruefbar')
    expect(issue?.severity).toBe('hinweis')
  })

  it('an explicit n.a. marker on a required input is distinguished from a genuinely empty cell', () => {
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [mkRow({ fek: 1, mss: null, rawText: { mss: 'n.a.' } } as Partial<QAFRow>)],
    }
    const results = evaluateBusinessRules(input)
    const r = find(results, 'rule_calc_fek')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toContain('nicht anwendbar')
  })

  it('teileProZyklus = 0 (division guard) -> nicht_pruefbar, not a crash/Infinity', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [mkRow({ fek: 1, teileProZyklus: 0 })] }
    const results = evaluateBusinessRules(input)
    const r = find(results, 'rule_calc_fek')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(Number.isFinite(r?.expected ?? 0)).toBe(true)
  })
})

// ── MANUFACTURING: rule_calc_fk ─────────────────────────────────────────────

describe('evaluateBusinessRules — rule_calc_fk (FK[BW], Leitfaden S.31)', () => {
  it('matching FK -> bestanden', () => {
    // FK = FEK + RFGK * Zykluszeit/TeileProZyklus/3600 = 10 + 36*10/1/3600 = 10.1
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [mkRow({ fek: 10, rfgk: 36, zykluszeit: 10, teileProZyklus: 1, fk: 10.1 })],
    }
    const r = find(evaluateBusinessRules(input), 'rule_calc_fk')
    expect(r?.status).toBe('bestanden')
  })

  it('deviating FK -> abweichung, severity pruefen (fk[BW] is not a literal DETAIL_SUM_CHECKS field)', () => {
    const input: BusinessRuleInput = {
      side: 'NEU',
      steps: [mkRow({ fek: 10, rfgk: 36, zykluszeit: 10, teileProZyklus: 1, fk: 500 })],
    }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_fk')
    expect(issue?.severity).toBe('pruefen')
  })

  it('fk null -> skipped entirely', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [mkRow({ fk: null })] }
    expect(find(evaluateBusinessRules(input), 'rule_calc_fk')).toBeUndefined()
  })
})

// ── MANUFACTURING: rule_calc_ausschuss_fertigung ────────────────────────────

describe('evaluateBusinessRules — rule_calc_ausschuss_fertigung (Ausschusskosten Fertigung[AW], Leitfaden S.29 Abb.20)', () => {
  it('matching ausschusskosten -> bestanden', () => {
    // 100 * (1/(1-0.02) - 1) = 100 * (1/0.98 - 1) = 2.040816...
    const expected = 100 * (1 / (1 - 2 / 100) - 1)
    const input: BusinessRuleInput = { side: 'ALT', steps: [mkRow({ fkAW: 100, ausschuss: 2, ausschusskosten: expected })] }
    const r = find(evaluateBusinessRules(input), 'rule_calc_ausschuss_fertigung')
    expect(r?.status).toBe('bestanden')
  })

  it('deviating ausschusskosten -> abweichung, severity kritisch (literal DETAIL_SUM_CHECKS field)', () => {
    const input: BusinessRuleInput = {
      side: 'NEU',
      steps: [mkRow({ fkAW: 100, ausschuss: 2, ausschusskosten: 999 })],
    }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_ausschuss_fertigung')
    expect(issue?.severity).toBe('kritisch')
  })

  it('ausschuss >= 100% (nonsense input) -> nicht_pruefbar, not Infinity/NaN', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [mkRow({ fkAW: 100, ausschuss: 100, ausschusskosten: 1 })] }
    const r = find(evaluateBusinessRules(input), 'rule_calc_ausschuss_fertigung')
    expect(r?.status).toBe('nicht_pruefbar')
  })

  it('ausschusskosten null -> skipped entirely', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [mkRow({ ausschusskosten: null })] }
    expect(find(evaluateBusinessRules(input), 'rule_calc_ausschuss_fertigung')).toBeUndefined()
  })
})

// ── MATERIAL: rule_calc_material_cost_per_unit_aw ───────────────────────────

describe('evaluateBusinessRules — rule_calc_material_cost_per_unit_aw (Leitfaden S.19)', () => {
  it('matching costPerUnitAw -> bestanden', () => {
    // 1*1 + 0+0+0+0 = 1
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [],
      materialRows: [mkMaterialRow({ costPerUnitBw: 1, exchangeRate: 1, costPerUnitAw: 1 })],
    }
    const r = find(evaluateBusinessRules(input), 'rule_calc_material_cost_per_unit_aw')
    expect(r?.status).toBe('bestanden')
  })

  it('deviating costPerUnitAw -> abweichung, severity pruefen', () => {
    const input: BusinessRuleInput = {
      side: 'NEU',
      steps: [],
      materialRows: [mkMaterialRow({ costPerUnitBw: 1, exchangeRate: 1, costPerUnitAw: 999 })],
    }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_material_cost_per_unit_aw')
    expect(issue?.severity).toBe('pruefen')
  })

  it('costPerUnitAw null -> skipped entirely (e.g. RMR variable-share row)', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [], materialRows: [mkMaterialRow({ costPerUnitAw: null })] }
    expect(find(evaluateBusinessRules(input), 'rule_calc_material_cost_per_unit_aw')).toBeUndefined()
  })

  it('materialRows undefined (no MATERIAL parse attempted) -> no MATERIAL checks at all', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [] }
    const results = evaluateBusinessRules(input)
    expect(results.some((r) => r.checkId.startsWith('rule_calc_material'))).toBe(false)
  })

  it('materialRows null (MATERIAL sheet missing/degraded) -> one file-level nicht_pruefbar per MATERIAL check', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [], materialRows: null }
    const results = evaluateBusinessRules(input)
    const r = results.find((x) => x.checkId === 'rule_calc_material_cost_per_unit_aw')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toContain('MATERIAL-Sheet')
  })
})

// ── MATERIAL: rule_calc_material_scrap_cost ─────────────────────────────────

describe('evaluateBusinessRules — rule_calc_material_scrap_cost (Ausschusskosten Material[AW], Leitfaden S.17 Abb.10)', () => {
  it('matching scrapCost -> bestanden', () => {
    const expected = 100 * (1 / (1 - 4 / 100) - 1)
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [],
      materialRows: [mkMaterialRow({ materialCost: 100, scrapRate: 4, scrapCost: expected })],
    }
    const r = find(evaluateBusinessRules(input), 'rule_calc_material_scrap_cost')
    expect(r?.status).toBe('bestanden')
  })

  it('deviating scrapCost -> abweichung, severity pruefen (not a literal DETAIL_SUM_CHECKS field)', () => {
    const input: BusinessRuleInput = {
      side: 'NEU',
      steps: [],
      materialRows: [mkMaterialRow({ materialCost: 100, scrapRate: 4, scrapCost: 999 })],
    }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_material_scrap_cost')
    expect(issue?.severity).toBe('pruefen')
  })
})

// ── MATERIAL: rule_calc_raw_material_surcharge ──────────────────────────────

describe('evaluateBusinessRules — rule_calc_raw_material_surcharge (RoZ0[AW], Leitfaden S.17 Abb.9)', () => {
  it('matching RoZ0 -> bestanden', () => {
    // 2 * 0.5 * 10 = 10
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [],
      materialRows: [
        mkMaterialRow({
          unitOfMeasure: 'Raw_material_risk_(kg)',
          quantityPerQuotedPart: 2,
          referenceWeight: 0.5,
          rawMaterialQuotation: 10,
          rawMaterialSurcharge: 10,
        }),
      ],
    }
    const r = find(evaluateBusinessRules(input), 'rule_calc_raw_material_surcharge')
    expect(r?.status).toBe('bestanden')
  })

  it('rawMaterialSurcharge null (ordinary Kaufteil row without RMR) -> skipped entirely, no spam', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [], materialRows: [mkMaterialRow({ rawMaterialSurcharge: null })] }
    expect(find(evaluateBusinessRules(input), 'rule_calc_raw_material_surcharge')).toBeUndefined()
  })
})

// ── MATERIAL: rule_calc_material_cost (Kaufteil vs. Rohmaterial branch) ─────

describe('evaluateBusinessRules — rule_calc_material_cost (Kaufteilkosten/Rohmaterialkosten, Leitfaden S.19)', () => {
  it('Kaufteil branch: materialCost = costPerUnitAw * quantityPerQuotedPart -> bestanden', () => {
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [],
      materialRows: [
        mkMaterialRow({
          unitOfMeasure: 'Unit_(purchased_part)',
          costPerUnitAw: 5,
          quantityPerQuotedPart: 3,
          referenceQuantity: null,
          materialCost: 15,
        }),
      ],
    }
    const r = find(evaluateBusinessRules(input), 'rule_calc_material_cost')
    expect(r?.status).toBe('bestanden')
  })

  it('Rohmaterial branch: materialCost = (costPerUnitAw * referenceQuantity - rebate) * quantityPerQuotedPart -> bestanden', () => {
    // (2 * 10 - 1) * 3 = 57
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [],
      materialRows: [
        mkMaterialRow({
          unitOfMeasure: 'kg',
          costPerUnitAw: 2,
          referenceQuantity: 10,
          rebate: 1,
          quantityPerQuotedPart: 3,
          materialCost: 57,
        }),
      ],
    }
    const r = find(evaluateBusinessRules(input), 'rule_calc_material_cost')
    expect(r?.status).toBe('bestanden')
  })

  it('Rohmaterial branch with blank rebate -> nicht_pruefbar (NIE null-als-0)', () => {
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [],
      materialRows: [
        mkMaterialRow({ unitOfMeasure: 'kg', costPerUnitAw: 2, referenceQuantity: 10, rebate: null, materialCost: 20 }),
      ],
    }
    const r = find(evaluateBusinessRules(input), 'rule_calc_material_cost')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toContain('ckverg')
  })

  it('RMR variable-share row (Raw_material_risk_(kg)) -> nicht_pruefbar, documented business reason', () => {
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [],
      materialRows: [mkMaterialRow({ unitOfMeasure: 'Raw_material_risk_(kg)', materialCost: 5 })],
    }
    const r = find(evaluateBusinessRules(input), 'rule_calc_material_cost')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toContain('RMR')
  })

  it('deviating Kaufteilkosten -> abweichung, severity kritisch (literal material_detail_sum field)', () => {
    const input: BusinessRuleInput = {
      side: 'NEU',
      steps: [],
      materialRows: [
        mkMaterialRow({
          unitOfMeasure: 'Unit_(purchased_part)',
          costPerUnitAw: 5,
          quantityPerQuotedPart: 3,
          referenceQuantity: null,
          materialCost: 999,
        }),
      ],
    }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_material_cost')
    expect(issue?.severity).toBe('kritisch')
  })

  it('materialCost null -> skipped entirely', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [], materialRows: [mkMaterialRow({ materialCost: null })] }
    expect(find(evaluateBusinessRules(input), 'rule_calc_material_cost')).toBeUndefined()
  })
})

// ── SBM: rule_calc_sbm_tool_fixture_cost ────────────────────────────────────

describe('evaluateBusinessRules — rule_calc_sbm_tool_fixture_cost (Summe Werkzeug-/Vorrichtungskosten[AW], teil2 S.37)', () => {
  it('matching total -> bestanden', () => {
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [],
      sbmRows: [mkSbmRow({ toolFixtureCostBw: 100, exchangeRate: 1.1, toolFixtureCount: 2, totalToolFixtureCostAw: 220 })],
    }
    const r = find(evaluateBusinessRules(input), 'rule_calc_sbm_tool_fixture_cost')
    expect(r?.status).toBe('bestanden')
  })

  it('deviating total -> abweichung, severity kritisch (literal sbm_detail_sum field)', () => {
    const input: BusinessRuleInput = {
      side: 'NEU',
      steps: [],
      sbmRows: [mkSbmRow({ toolFixtureCostBw: 100, exchangeRate: 1.1, toolFixtureCount: 2, totalToolFixtureCostAw: 999 })],
    }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_sbm_tool_fixture_cost')
    expect(issue?.severity).toBe('kritisch')
  })

  it('sbmRows undefined -> no SBM checks at all', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [] }
    expect(evaluateBusinessRules(input).some((r) => r.checkId === 'rule_calc_sbm_tool_fixture_cost')).toBe(false)
  })

  it('sbmRows null -> one file-level nicht_pruefbar', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [], sbmRows: null }
    const r = find(evaluateBusinessRules(input), 'rule_calc_sbm_tool_fixture_cost', -1)
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toContain('SBM')
  })

  it('totalToolFixtureCostAw null -> skipped entirely', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [], sbmRows: [mkSbmRow({ totalToolFixtureCostAw: null })] }
    expect(find(evaluateBusinessRules(input), 'rule_calc_sbm_tool_fixture_cost')).toBeUndefined()
  })
})

// ── businessRuleResultToPlausibilityIssue — formula-engine synergy text ────

describe('businessRuleResultToPlausibilityIssue — formula-engine link (text-only, P2.1 <-> P2.2 synergy)', () => {
  it('appends a linked-formula-change sentence when a linkedFormulaChangeExplanation is supplied for an abweichung', () => {
    const input: BusinessRuleInput = { side: 'NEU', steps: [mkRow({ fek: 999 })] }
    const r = find(evaluateBusinessRules(input), 'rule_calc_fek')!
    const issue = businessRuleResultToPlausibilityIssue(r, 'Formel geändert: ALT "=A1*B1" -> NEU "=A1*B1*1.5"')
    expect(issue?.explanation).toContain('Formel geändert')
    expect(issue?.explanation).toContain('starker Manipulations-Indikator')
  })

  it('does not append anything when no linkedFormulaChangeExplanation is supplied', () => {
    const input: BusinessRuleInput = { side: 'NEU', steps: [mkRow({ fek: 999 })] }
    const r = find(evaluateBusinessRules(input), 'rule_calc_fek')!
    const issue = businessRuleResultToPlausibilityIssue(r)
    expect(issue?.explanation).not.toContain('Manipulations-Indikator')
  })

  it('never appends the link text to a nicht_pruefbar or bestanden result', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [mkRow({ fek: 1, lohnkosten: null })] }
    const r = find(evaluateBusinessRules(input), 'rule_calc_fek')!
    const issue = businessRuleResultToPlausibilityIssue(r, 'irrelevant')
    expect(issue?.explanation).not.toContain('Manipulations-Indikator')
  })
})

// KAR-906/P3.2: every rule_calc_* issue (abweichung AND nicht_pruefbar)
// carries an EN counterpart via explanationEn, field names sourced from the
// canonical registry (not hand-translated).
describe('checkBusinessRules / businessRuleResultToPlausibilityIssue — bilingual (KAR-906)', () => {
  it('an "abweichung" issue carries explanationEn distinct from explanation', () => {
    const input: BusinessRuleInput = { side: 'NEU', steps: [mkRow({ fek: 999 })] }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_fek')
    expect(issue?.explanationEn).toBeTruthy()
    expect(issue?.explanationEn).not.toBe(issue?.explanation)
    expect(issue?.explanationEn).toContain('999')
  })

  it('a "nicht_pruefbar" issue names the missing field in English via the canonical registry', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [mkRow({ fek: 1, lohnkosten: null })] }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_fek_nicht_pruefbar')
    expect(issue?.explanationEn).toMatch(/Input field/)
    expect(issue?.explanationEn).toMatch(/labor cost/i)
  })

  it('a "nicht anwendbar" nicht_pruefbar reason is distinguished in English too', () => {
    const input: BusinessRuleInput = {
      side: 'ALT',
      steps: [mkRow({ fek: 1, mss: null, rawText: { mss: 'n.a.' } } as Partial<QAFRow>)],
    }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_fek_nicht_pruefbar')
    expect(issue?.explanationEn).toMatch(/marked "not applicable"/)
  })

  it('a MATERIAL row check carries an English reason built from the MATERIAL canonical labels', () => {
    const rows: MaterialRow[] = [mkMaterialRow({ costPerUnitAw: 5, costPerUnitBw: null })]
    const input: BusinessRuleInput = { side: 'ALT', steps: [], materialRows: rows }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_material_cost_per_unit_aw_nicht_pruefbar')
    expect(issue?.explanationEn).toMatch(/Input field/)
  })

  it('the MATERIAL file-level nicht_pruefbar (no sheet at all) has an English reason', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [], materialRows: null }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_material_cost_nicht_pruefbar')
    expect(issue?.explanationEn).toMatch(/No MATERIAL sheet detected/)
  })

  it('the SBM file-level nicht_pruefbar (no sheet at all) has an English reason', () => {
    const input: BusinessRuleInput = { side: 'ALT', steps: [], sbmRows: null }
    const issue = checkBusinessRules(input).find((i) => i.type === 'rule_calc_sbm_tool_fixture_cost_nicht_pruefbar')
    expect(issue?.explanationEn).toMatch(/No SBM sheet detected/)
  })
})

describe('BUSINESS_RULES_CONFIG', () => {
  it('exposes a relative + absolute tolerance, same shape as RECONCILIATION_CONFIG', () => {
    expect(BUSINESS_RULES_CONFIG.relativeTolerance).toBeGreaterThan(0)
    expect(BUSINESS_RULES_CONFIG.absoluteToleranceMinor).toBeGreaterThanOrEqual(0)
  })
})
