// Tests for the Summen-Rekonziliation Summary <-> Fertigungskosten-Zeilen
// (KAR-887 / P0.2).
//
// Fixtures are entirely synthetic (invented field values) — no real
// Brose/Kiekert/Autoliv data or files.

import { describe, it, expect } from 'vitest'
import type { QAFRow } from '@/lib/qaf-parser'
import { metricsParse } from './summary-fixtures'
import {
  evaluateReconciliation,
  checkReconciliation,
  RECONCILIATION_CONFIG,
  type ReconciliationInput,
} from '../reconciliation'
import type { MaterialRow, MaterialRowValues } from '../material-parser'
import type { SbmRow, SbmRowValues } from '../sbm-parser'
import type { LogisticsRow, LogisticsRowValues } from '../logistics-parser'

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: 1,
    referenceQuantity: null,
    netQuantity: null,
    rebate: null,
    quantityPerQuotedPart: 1,
    materialCost: 100,
    scrapRate: 0,
    scrapCost: 0,
    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: 100,
    totalSbmToolCostAw: null,
    devicesFollowupMaintenancePerUnit: null,
  }
  return {
    ...base,
    ...o,
    sourceCells: {},
    normalized: {},
    rawText: (o as SbmRow).rawText ?? {},
    deviceClassification: (o as SbmRow).deviceClassification ?? null,
  }
}

function mkLogisticsRow(o: Partial<LogisticsRowValues> = {}): LogisticsRow {
  const base: LogisticsRowValues = {
    positionNumber: '1',
    deliverySite: 'Spartanburg',
    totalVolume: 50000,
    quotationCurrency: 'EUR',
    transportCostPerPart: 2.0,
    deliveryTerms: 'FCA',
    packagingCostPerPart: 0.5,
    surchargeKeyPackaging: 'VERP',
    prePackagingCostPerPart: 0.3,
    surchargeKeyPrePackaging: 'LVVV',
    logisticsCostPerDeliverySite: 2.8,
    customsCostPerPart: 0.4,
    customsTariffNumber: '8708299000',
    customsCostPerDeliverySite: 0.4,
  }
  return { ...base, ...o, sourceCells: {}, normalized: {}, rawText: (o as LogisticsRow).rawText ?? {} }
}

function mkRow(o: 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,
    ...o,
  }
}

describe('evaluateReconciliation — fk_detail_sum (Sigma Fertigungskosten-Zeilen vs. Summary)', () => {
  it('sums fkAW across rows and matches the summary within tolerance -> bestanden, no issue', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: 100 }), mkRow({ fkAW: 200 })],
      summaryMetrics: metricsParse({ manufacturingCosts: 300 }),
    }
    const results = evaluateReconciliation(input)
    const r = results.find((x) => x.checkId === 'fk_detail_sum')
    expect(r?.status).toBe('bestanden')
    expect(r?.expected).toBe(300)
    expect(r?.actual).toBe(300)
    expect(checkReconciliation(input).some((i) => i.type === 'recon_fk_detail_sum')).toBe(false)
  })

  it('a clear mismatch beyond tolerance produces a "pruefen" issue with concrete numbers in DE text', () => {
    const input: ReconciliationInput = {
      side: 'NEU',
      steps: [mkRow({ fkAW: 100 }), mkRow({ fkAW: 200 })],
      summaryMetrics: metricsParse({ manufacturingCosts: 500 }),
    }
    const results = evaluateReconciliation(input)
    const r = results.find((x) => x.checkId === 'fk_detail_sum')
    expect(r?.status).toBe('abweichung')
    expect(r?.expected).toBe(300)
    expect(r?.actual).toBe(500)
    expect(r?.deltaAbsolute).toBe(200)

    const issues = checkReconciliation(input)
    const issue = issues.find((i) => i.type === 'recon_fk_detail_sum')
    expect(issue?.severity).toBe('pruefen')
    expect(issue?.explanation).toContain('300')
    expect(issue?.explanation).toContain('500')
    expect(issue?.explanation).toContain('200')
  })

  it('stays within the configured relative tolerance -> bestanden (0.5% default)', () => {
    const expected = 10000
    const withinDelta = expected * (RECONCILIATION_CONFIG.relativeTolerance * 0.9) // just inside 0.5%
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: expected })],
      summaryMetrics: metricsParse({ manufacturingCosts: expected + withinDelta }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'fk_detail_sum')
    expect(r?.status).toBe('bestanden')
  })

  it('breaks just outside the configured relative tolerance -> abweichung', () => {
    const expected = 10000
    const outsideDelta = expected * (RECONCILIATION_CONFIG.relativeTolerance * 1.5)
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: expected })],
      summaryMetrics: metricsParse({ manufacturingCosts: expected + outsideDelta }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'fk_detail_sum')
    expect(r?.status).toBe('abweichung')
  })

  it('a row explicitly marked n.a. on fkAW contributes 0, does not block the check', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: 100 }), mkRow({ fkAW: null, rawText: { fkAW: 'n.a.' } })],
      summaryMetrics: metricsParse({ manufacturingCosts: 100 }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'fk_detail_sum')
    expect(r?.status).toBe('bestanden')
    expect(r?.expected).toBe(100)
  })

  it('a genuinely missing/unparsable fkAW value (no n.a. marker) makes the check nicht_pruefbar, not bestanden and not a violation', () => {
    const input: ReconciliationInput = {
      side: 'NEU',
      steps: [mkRow({ fkAW: 100 }), mkRow({ fkAW: null, rawText: { fkAW: 'siehe Anlage 3' } })],
      summaryMetrics: metricsParse({ manufacturingCosts: 100 }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'fk_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toBeTruthy()

    const issues = checkReconciliation(input)
    const issue = issues.find((i) => i.type === 'recon_fk_detail_sum_nicht_pruefbar')
    expect(issue?.severity).toBe('hinweis')
  })

  it('a genuinely blank fkAW (no rawText at all) also makes the check nicht_pruefbar', () => {
    const input: ReconciliationInput = {
      side: 'NEU',
      steps: [mkRow({ fkAW: 100 }), mkRow({ fkAW: null })],
      summaryMetrics: metricsParse({ manufacturingCosts: 100 }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'fk_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })

  it('missing summary metric (manufacturingCosts null) -> nicht_pruefbar, never a false "bestanden"', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: 100 })],
      summaryMetrics: metricsParse({}), // manufacturingCosts absent
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'fk_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toMatch(/Fertigungskosten/)
  })

  it('no Fertigungskosten rows at all -> nicht_pruefbar (no basis, not an automatic violation)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ manufacturingCosts: 500 }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'fk_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })

  it('row currency diverging from the summary currency -> nicht_pruefbar (summation not reliable across currencies)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: 100, angebotswaehrung: 'CNY' })],
      summaryMetrics: metricsParse({ manufacturingCosts: 100 }), // fixture currency is EUR
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'fk_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })
})

describe('evaluateReconciliation — scrap_manufacturing_detail_sum (Sigma Ausschusskosten Fertigung vs. Summary)', () => {
  it('matches within tolerance -> bestanden', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ ausschusskosten: 1 }), mkRow({ ausschusskosten: 2 })],
      summaryMetrics: metricsParse({ scrapManufacturing: 3 }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'scrap_manufacturing_detail_sum')
    expect(r?.status).toBe('bestanden')
  })

  it('mismatch beyond tolerance -> abweichung, severity pruefen', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ ausschusskosten: 1 })],
      summaryMetrics: metricsParse({ scrapManufacturing: 50 }),
    }
    const issues = checkReconciliation(input)
    const issue = issues.find((i) => i.type === 'recon_scrap_manufacturing_detail_sum')
    expect(issue?.severity).toBe('pruefen')
  })
})

describe('evaluateReconciliation — material_detail_sum (Sigma MATERIAL-Zeilen vs. Summary, KAR-897/P1.6)', () => {
  it('is entirely omitted when materialRows is undefined (pre-P1.6 / rehydrated callers stay unaffected)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ materialCosts: 100 }),
      // materialRows intentionally omitted
    }
    const results = evaluateReconciliation(input)
    expect(results.find((r) => r.checkId === 'material_detail_sum')).toBeUndefined()
  })

  it('materialRows: null (MATERIAL sheet not found/unusable) -> nicht_pruefbar with a reason', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ materialCosts: 100 }),
      materialRows: null,
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'material_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toMatch(/MATERIAL-Sheet/)
  })

  it('sums materialCost across rows and matches the summary within tolerance -> bestanden', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ materialCosts: 300 }),
      materialRows: [mkMaterialRow({ materialCost: 100 }), mkMaterialRow({ materialCost: 200 })],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'material_detail_sum')
    expect(r?.status).toBe('bestanden')
    expect(r?.expected).toBe(300)
    expect(r?.actual).toBe(300)
    expect(checkReconciliation(input).some((i) => i.type === 'recon_material_detail_sum')).toBe(false)
  })

  it('sums RMR fixed+variable split rows together (both contribute to the same position)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ materialCosts: 150 }),
      materialRows: [
        mkMaterialRow({ positionNumber: '2', materialCost: 100, unitOfMeasure: 'kg' }),
        mkMaterialRow({ positionNumber: '2', materialCost: 50, unitOfMeasure: 'Raw_material_risk_(kg)' }),
      ],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'material_detail_sum')
    expect(r?.status).toBe('bestanden')
    expect(r?.expected).toBe(150)
  })

  it('a clear mismatch beyond tolerance produces a "pruefen" issue with concrete numbers', () => {
    const input: ReconciliationInput = {
      side: 'NEU',
      steps: [],
      summaryMetrics: metricsParse({ materialCosts: 500 }),
      materialRows: [mkMaterialRow({ materialCost: 100 }), mkMaterialRow({ materialCost: 200 })],
    }
    const issues = checkReconciliation(input)
    const issue = issues.find((i) => i.type === 'recon_material_detail_sum')
    expect(issue?.severity).toBe('pruefen')
    expect(issue?.explanation).toContain('300')
    expect(issue?.explanation).toContain('500')
  })

  it('empty materialRows array (sheet found but 0 usable rows) -> nicht_pruefbar', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ materialCosts: 100 }),
      materialRows: [],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'material_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })

  it('missing summary metric (materialCosts null) -> nicht_pruefbar', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({}),
      materialRows: [mkMaterialRow({ materialCost: 100 })],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'material_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toMatch(/Materialkosten/)
  })

  it('a row explicitly marked n.a. contributes 0, does not block the check', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ materialCosts: 100 }),
      materialRows: [
        mkMaterialRow({ materialCost: 100 }),
        { ...mkMaterialRow({ materialCost: null }), rawText: { materialCost: 'n.a.' } },
      ],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'material_detail_sum')
    expect(r?.status).toBe('bestanden')
  })

  it('quotation currency diverging from the summary currency -> nicht_pruefbar', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ materialCosts: 100 }), // fixture currency EUR
      materialRows: [mkMaterialRow({ materialCost: 100, quotationCurrency: 'CNY' })],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'material_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })
})

describe('evaluateReconciliation — sbm_detail_sum (Sigma SBM-Zeilen vs. Summary.devicesAndTools, KAR-898/P1.7)', () => {
  it('is entirely omitted when sbmRows is undefined (pre-P1.7 / rehydrated callers stay unaffected)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ devicesAndTools: 100 }),
      // sbmRows intentionally omitted
    }
    const results = evaluateReconciliation(input)
    expect(results.find((r) => r.checkId === 'sbm_detail_sum')).toBeUndefined()
  })

  it('sbmRows: null (SBM sheet not found/unusable) but summary devicesAndTools has a real value -> nicht_pruefbar', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ devicesAndTools: 100 }),
      sbmRows: null,
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'sbm_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toMatch(/SBM-Sheet/)
  })

  it('sbmRows: null AND devicesAndTools missing (both empty) -> nicht_pruefbar, NOT bestanden (adversarial-review finding, KAR-898 follow-up: null must never silently pass as "unauffaellig" even when the summary side is also empty — the "both sides empty -> bestanden" shortcut applies ONLY to a CONFIRMED-empty array, see reconciliation.ts module header)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({}),
      sbmRows: null,
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'sbm_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toMatch(/SBM-Sheet/)
  })

  it('sbmRows: null AND devicesAndTools === 0 (both empty) -> nicht_pruefbar, NOT bestanden (same as above)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ devicesAndTools: 0 }),
      sbmRows: null,
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'sbm_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })

  it('sums totalToolFixtureCostAw across rows and matches the summary within tolerance -> bestanden', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ devicesAndTools: 300 }),
      sbmRows: [mkSbmRow({ totalToolFixtureCostAw: 100 }), mkSbmRow({ totalToolFixtureCostAw: 200 })],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'sbm_detail_sum')
    expect(r?.status).toBe('bestanden')
    expect(r?.expected).toBe(300)
    expect(r?.actual).toBe(300)
    expect(checkReconciliation(input).some((i) => i.type === 'recon_sbm_detail_sum')).toBe(false)
  })

  it('a clear mismatch beyond tolerance produces a "pruefen" issue with concrete numbers', () => {
    const input: ReconciliationInput = {
      side: 'NEU',
      steps: [],
      summaryMetrics: metricsParse({ devicesAndTools: 500 }),
      sbmRows: [mkSbmRow({ totalToolFixtureCostAw: 100 }), mkSbmRow({ totalToolFixtureCostAw: 200 })],
    }
    const issues = checkReconciliation(input)
    const issue = issues.find((i) => i.type === 'recon_sbm_detail_sum')
    expect(issue?.severity).toBe('pruefen')
    expect(issue?.explanation).toContain('300')
    expect(issue?.explanation).toContain('500')
  })

  it('empty sbmRows array WHILE summary devicesAndTools has a real value -> nicht_pruefbar (cannot reconcile, not unauffaellig)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ devicesAndTools: 100 }),
      sbmRows: [],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'sbm_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })

  it('empty sbmRows array AND devicesAndTools missing/0 (both empty) -> bestanden ("unauffaellig") — a CONFIRMED-empty array (sheet parsed, header intact, zero rows), unlike sbmRows: null above, which never shares this shortcut', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({}),
      sbmRows: [],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'sbm_detail_sum')
    expect(r?.status).toBe('bestanden')
    expect(checkReconciliation(input).some((i) => i.type.startsWith('recon_sbm_detail_sum'))).toBe(false)
  })

  it('a row explicitly marked n.a. contributes 0, does not block the check', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ devicesAndTools: 100 }),
      sbmRows: [
        mkSbmRow({ totalToolFixtureCostAw: 100 }),
        { ...mkSbmRow({ totalToolFixtureCostAw: null }), rawText: { totalToolFixtureCostAw: 'n.a.' } },
      ],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'sbm_detail_sum')
    expect(r?.status).toBe('bestanden')
  })

  it('quotation currency diverging from the summary currency -> nicht_pruefbar (fkAW-Lehre: never sum across currencies)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ devicesAndTools: 100 }), // fixture currency EUR
      sbmRows: [mkSbmRow({ totalToolFixtureCostAw: 100, quotationCurrency: 'CNY' })],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'sbm_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })
})

describe('evaluateReconciliation — logistics_transport_detail_sum (Sigma transportCostPerPart vs. Summary.transportSupplierToBMW, KAR-903/P2.4; post-merge adversarial-review fix confidence 82: transport-only, NOT the combined logisticsCostPerDeliverySite — see reconciliation.ts module header "Evidenzlage + Korrektur")', () => {
  it('is entirely omitted when logisticsRows is undefined (pre-P2.4 / rehydrated callers stay unaffected)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ transportSupplierToBMW: 100 }),
      // logisticsRows intentionally omitted
    }
    const results = evaluateReconciliation(input)
    expect(results.find((r) => r.checkId === 'logistics_transport_detail_sum')).toBeUndefined()
  })

  it('logisticsRows: null (LOGISTICS sheet not found/unusable) but summary transportSupplierToBMW has a real value -> nicht_pruefbar', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ transportSupplierToBMW: 100 }),
      logisticsRows: null,
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_transport_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toMatch(/LOGISTICS/)
  })

  it('logisticsRows: null AND transportSupplierToBMW missing (both empty) -> nicht_pruefbar, NOT bestanden (same coreFieldsFound-Schutz as sbm_detail_sum, #274/KAR-898 lesson: null never silently shares the "both sides empty" shortcut)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({}),
      logisticsRows: null,
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_transport_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })

  it('sums transportCostPerPart across rows and matches the summary within tolerance -> bestanden', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ transportSupplierToBMW: 300 }),
      logisticsRows: [
        mkLogisticsRow({ transportCostPerPart: 100 }),
        mkLogisticsRow({ transportCostPerPart: 200 }),
      ],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_transport_detail_sum')
    expect(r?.status).toBe('bestanden')
    expect(r?.expected).toBe(300)
    expect(r?.actual).toBe(300)
    expect(checkReconciliation(input).some((i) => i.type === 'recon_logistics_transport_detail_sum')).toBe(false)
  })

  it('locks the confirmed transport-only semantics: a row whose logisticsCostPerDeliverySite (Transport+Verpackung+Vorverpackung) differs from transportCostPerPart alone must sum ONLY the transport component (fix for the post-merge finding: earlier code summed the combined 3-component field against this transport-only Summary metric, which would have systematically fired abweichung on every VERP/LVVV overseas-supplier file)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      // transportSupplierToBMW matches the TRANSPORT-ONLY sum (100), not the
      // combined logisticsCostPerDeliverySite sum (100+50+30=180) — a mock
      // that would previously have produced a false "abweichung".
      summaryMetrics: metricsParse({ transportSupplierToBMW: 100 }),
      logisticsRows: [
        mkLogisticsRow({
          transportCostPerPart: 100,
          packagingCostPerPart: 50,
          prePackagingCostPerPart: 30,
          logisticsCostPerDeliverySite: 180,
        }),
      ],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_transport_detail_sum')
    expect(r?.status).toBe('bestanden')
    expect(r?.expected).toBe(100)
    expect(checkReconciliation(input).some((i) => i.type === 'recon_logistics_transport_detail_sum')).toBe(false)
  })

  it('a clear mismatch beyond tolerance produces a "pruefen" issue with concrete numbers', () => {
    const input: ReconciliationInput = {
      side: 'NEU',
      steps: [],
      summaryMetrics: metricsParse({ transportSupplierToBMW: 500 }),
      logisticsRows: [
        mkLogisticsRow({ transportCostPerPart: 100 }),
        mkLogisticsRow({ transportCostPerPart: 200 }),
      ],
    }
    const issues = checkReconciliation(input)
    const issue = issues.find((i) => i.type === 'recon_logistics_transport_detail_sum')
    expect(issue?.severity).toBe('pruefen')
    expect(issue?.explanation).toContain('300')
    expect(issue?.explanation).toContain('500')
  })

  it('empty logisticsRows array WHILE summary transportSupplierToBMW has a real value -> nicht_pruefbar (cannot reconcile, not unauffaellig)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ transportSupplierToBMW: 100 }),
      logisticsRows: [],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_transport_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })

  it('empty logisticsRows array AND transportSupplierToBMW missing/0 (both empty) -> bestanden ("unauffaellig") — a CONFIRMED-empty array (sheet parsed, header intact, zero rows), unlike logisticsRows: null above', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({}),
      logisticsRows: [],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_transport_detail_sum')
    expect(r?.status).toBe('bestanden')
    expect(checkReconciliation(input).some((i) => i.type.startsWith('recon_logistics_transport_detail_sum'))).toBe(false)
  })

  it('a row explicitly marked n.a. contributes 0, does not block the check', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ transportSupplierToBMW: 100 }),
      logisticsRows: [
        mkLogisticsRow({ transportCostPerPart: 100 }),
        {
          ...mkLogisticsRow({ transportCostPerPart: null }),
          rawText: { transportCostPerPart: 'n.a.' },
        },
      ],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_transport_detail_sum')
    expect(r?.status).toBe('bestanden')
  })

  it('quotation currency diverging from the summary currency -> nicht_pruefbar (fkAW-Lehre: never sum across currencies)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ transportSupplierToBMW: 100 }), // fixture currency EUR
      logisticsRows: [mkLogisticsRow({ transportCostPerPart: 100, quotationCurrency: 'CNY' })],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_transport_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })
})

describe('evaluateReconciliation — logistics_customs_detail_sum (Sigma customsCostPerDeliverySite vs. Summary.customsSupplierToBMW, KAR-903/P2.4)', () => {
  it('is entirely omitted when logisticsRows is undefined', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ customsSupplierToBMW: 100 }),
    }
    const results = evaluateReconciliation(input)
    expect(results.find((r) => r.checkId === 'logistics_customs_detail_sum')).toBeUndefined()
  })

  it('logisticsRows: null but summary customsSupplierToBMW has a real value -> nicht_pruefbar', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ customsSupplierToBMW: 100 }),
      logisticsRows: null,
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_customs_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toMatch(/LOGISTICS/)
  })

  it('logisticsRows: null AND customsSupplierToBMW missing (both empty) -> nicht_pruefbar, NOT bestanden (coreFieldsFound-Schutz)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({}),
      logisticsRows: null,
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_customs_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })

  it('sums customsCostPerDeliverySite across rows and matches the summary within tolerance -> bestanden', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ customsSupplierToBMW: 30 }),
      logisticsRows: [
        mkLogisticsRow({ customsCostPerDeliverySite: 10 }),
        mkLogisticsRow({ customsCostPerDeliverySite: 20 }),
      ],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_customs_detail_sum')
    expect(r?.status).toBe('bestanden')
    expect(r?.expected).toBe(30)
    expect(r?.actual).toBe(30)
  })

  it('a clear mismatch beyond tolerance produces a "pruefen" issue', () => {
    const input: ReconciliationInput = {
      side: 'NEU',
      steps: [],
      summaryMetrics: metricsParse({ customsSupplierToBMW: 50 }),
      logisticsRows: [
        mkLogisticsRow({ customsCostPerDeliverySite: 10 }),
        mkLogisticsRow({ customsCostPerDeliverySite: 20 }),
      ],
    }
    const issues = checkReconciliation(input)
    const issue = issues.find((i) => i.type === 'recon_logistics_customs_detail_sum')
    expect(issue?.severity).toBe('pruefen')
  })

  it('empty logisticsRows array AND customsSupplierToBMW missing/0 (both empty) -> bestanden ("unauffaellig")', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({}),
      logisticsRows: [],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_customs_detail_sum')
    expect(r?.status).toBe('bestanden')
  })

  it('quotation currency diverging from the summary currency -> nicht_pruefbar', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ customsSupplierToBMW: 10 }),
      logisticsRows: [mkLogisticsRow({ customsCostPerDeliverySite: 10, quotationCurrency: 'CNY' })],
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'logistics_customs_detail_sum')
    expect(r?.status).toBe('nicht_pruefbar')
  })
})

describe('evaluateReconciliation — Kaskaden-Rekonziliation (nur Summary-Kennzahlen, keine Detail-Zeilen)', () => {
  it('SUMME HERSTELLKOSTEN = Materialkosten + Fertigungskosten -> bestanden within tolerance', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ materialCosts: 400, manufacturingCosts: 300, totalProductionCosts: 700 }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'herstellkosten_cascade')
    expect(r?.status).toBe('bestanden')
    expect(r?.expected).toBe(700)
  })

  it('SUMME HERSTELLKOSTEN broken beyond tolerance -> kritisch', () => {
    const input: ReconciliationInput = {
      side: 'NEU',
      steps: [],
      summaryMetrics: metricsParse({ materialCosts: 400, manufacturingCosts: 300, totalProductionCosts: 900 }),
    }
    const issues = checkReconciliation(input)
    const issue = issues.find((i) => i.type === 'recon_herstellkosten_cascade')
    expect(issue?.severity).toBe('kritisch')
    expect(issue?.explanation).toContain('700')
    expect(issue?.explanation).toContain('900')
  })

  it('missing component (materialCosts null) -> nicht_pruefbar, names the missing metric', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ manufacturingCosts: 300, totalProductionCosts: 700 }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'herstellkosten_cascade')
    expect(r?.status).toBe('nicht_pruefbar')
    expect(r?.reason).toMatch(/Materialkosten/)
  })

  it('GESAMTKOSTEN = Herstellkosten + Vorrichtungen/Folgewerkzeuge + Ausschusskosten Material + Fertigung', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({
        totalProductionCosts: 700,
        devicesAndTools: 50,
        scrapMaterial: 10,
        scrapManufacturing: 5,
        totalCosts: 765,
      }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'gesamtkosten_cascade')
    expect(r?.status).toBe('bestanden')
  })

  it('ANGEBOTSBASISPREIS = Gesamtkosten + Sonstige Zuschlaege', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({ totalCosts: 765, otherSurcharges: 35, quotationBasePrice: 800 }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'angebotsbasispreis_cascade')
    expect(r?.status).toBe('bestanden')
  })

  it('ANGEBOTSPREIS = Angebotsbasispreis + Rohstoffpreisanteile + Zoelle + Logistik', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({
        quotationBasePrice: 800,
        rawMaterialPriceShareMaterial: 10,
        rawMaterialPriceShareEnergy: 5,
        customsSupplierToBMW: 2,
        transportSupplierToBMW: 3,
        quotationPrice: 820,
      }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'angebotspreis_cascade')
    expect(r?.status).toBe('bestanden')
  })

  it('ANGEBOTSPREIS: a legacy template without rawMaterialPriceShareEnergy is nicht_pruefbar, never silently treated as 0', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({
        quotationBasePrice: 800,
        rawMaterialPriceShareMaterial: 10,
        // rawMaterialPriceShareEnergy intentionally absent (null)
        customsSupplierToBMW: 2,
        transportSupplierToBMW: 3,
        quotationPrice: 815,
      }),
    }
    const r = evaluateReconciliation(input).find((x) => x.checkId === 'angebotspreis_cascade')
    expect(r?.status).toBe('nicht_pruefbar')
  })

  it('all four cascade checks run independently — one nicht_pruefbar does not suppress the others', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [],
      summaryMetrics: metricsParse({
        materialCosts: 400,
        manufacturingCosts: 300,
        totalProductionCosts: 700,
        devicesAndTools: 50,
        scrapMaterial: 10,
        scrapManufacturing: 5,
        totalCosts: 765,
        // otherSurcharges intentionally absent -> angebotsbasispreis_cascade nicht_pruefbar
        quotationBasePrice: 800,
      }),
    }
    const results = evaluateReconciliation(input)
    expect(results.find((r) => r.checkId === 'herstellkosten_cascade')?.status).toBe('bestanden')
    expect(results.find((r) => r.checkId === 'gesamtkosten_cascade')?.status).toBe('bestanden')
    expect(results.find((r) => r.checkId === 'angebotsbasispreis_cascade')?.status).toBe('nicht_pruefbar')
  })
})

describe('evaluateReconciliation — determinism', () => {
  it('running the same input twice produces identical results (pure function)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: 100 }), mkRow({ fkAW: 200 })],
      summaryMetrics: metricsParse({
        materialCosts: 400,
        manufacturingCosts: 300,
        totalProductionCosts: 700,
      }),
    }
    expect(evaluateReconciliation(input)).toEqual(evaluateReconciliation(input))
    expect(checkReconciliation(input)).toEqual(checkReconciliation(input))
  })
})

describe('checkReconciliation — PlausibilityIssue mapping', () => {
  it('carries the side in the step field (mirrors rule-engine pattern)', () => {
    const input: ReconciliationInput = {
      side: 'NEU',
      steps: [mkRow({ fkAW: 999 })],
      summaryMetrics: metricsParse({ manufacturingCosts: 1 }),
    }
    const issue = checkReconciliation(input).find((i) => i.type === 'recon_fk_detail_sum')
    expect(issue?.step).toBe('NEU')
  })

  it('bestanden checks never produce an issue (other checks with no basis stay nicht_pruefbar/hinweis, not silently dropped)', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: 100 })],
      summaryMetrics: metricsParse({ manufacturingCosts: 100 }),
    }
    const issues = checkReconciliation(input)
    expect(issues.some((i) => i.type === 'recon_fk_detail_sum')).toBe(false)
    // Everything else has no data basis (only manufacturingCosts was provided)
    // and must be reported transparently, never silently — hinweis, not a violation.
    expect(issues.every((i) => i.type.endsWith('_nicht_pruefbar') && i.severity === 'hinweis')).toBe(true)
  })

  it('a fully specified, internally consistent summary produces zero issues at all', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: 300, ausschusskosten: 5 })],
      summaryMetrics: metricsParse({
        materialCosts: 400,
        manufacturingCosts: 300,
        totalProductionCosts: 700,
        devicesAndTools: 50,
        scrapMaterial: 10,
        scrapManufacturing: 5,
        totalCosts: 765,
        otherSurcharges: 35,
        quotationBasePrice: 800,
        rawMaterialPriceShareMaterial: 10,
        rawMaterialPriceShareEnergy: 5,
        customsSupplierToBMW: 2,
        transportSupplierToBMW: 3,
        quotationPrice: 820,
      }),
    }
    expect(checkReconciliation(input)).toEqual([])
  })
})

// KAR-906/P3.2: every recon_* issue (abweichung AND nicht_pruefbar) carries
// an EN counterpart via explanationEn.
describe('checkReconciliation — bilingual (KAR-906)', () => {
  it('an "abweichung" issue carries explanationEn distinct from explanation', () => {
    const input: ReconciliationInput = {
      side: 'NEU',
      steps: [mkRow({ fkAW: 999 })],
      summaryMetrics: metricsParse({ manufacturingCosts: 1 }),
    }
    const issue = checkReconciliation(input).find((i) => i.type === 'recon_fk_detail_sum')
    expect(issue?.explanationEn).toBeTruthy()
    expect(issue?.explanationEn).not.toBe(issue?.explanation)
    expect(issue?.explanationEn).toMatch(/summary states/i)
  })

  it('a "nicht_pruefbar" issue (missing metric) carries an English reason', () => {
    const input: ReconciliationInput = { side: 'ALT', steps: [], summaryMetrics: metricsParse({}) }
    const issue = checkReconciliation(input).find((i) => i.type === 'recon_fk_detail_sum_nicht_pruefbar')
    expect(issue?.explanationEn).toMatch(/not found in the summary/i)
  })

  it('a "nicht_pruefbar" issue (no MATERIAL sheet) carries an English reason', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: 100 })],
      summaryMetrics: metricsParse({ manufacturingCosts: 100 }),
      materialRows: null,
    }
    const issue = checkReconciliation(input).find((i) => i.type === 'recon_material_detail_sum_nicht_pruefbar')
    expect(issue?.explanationEn).toMatch(/No MATERIAL sheet detected/i)
  })

  it('a cascade "nicht_pruefbar" issue names the missing metrics in English too', () => {
    const input: ReconciliationInput = {
      side: 'ALT',
      steps: [mkRow({ fkAW: 100 })],
      summaryMetrics: metricsParse({ materialCosts: 400, manufacturingCosts: 300 }),
    }
    const issue = checkReconciliation(input).find((i) => i.type === 'recon_herstellkosten_cascade_nicht_pruefbar')
    expect(issue?.explanationEn).toMatch(/reconciliation not possible/i)
    expect(issue?.explanationEn).toMatch(/Total production costs/)
  })
})
