// Unit tests for container-assembly.ts (KAR-935 / Multi-QAF-Programm P2.1).
//
// FIXTURE-DATEN-REGEL (same discipline as synthetic-fixtures.ts's own header
// comment / header-parser.test.ts's own header comment, KAR-929 adversarial
// review F4 — merge-blocker class): every code, number, formula text, and
// cell reference below is FREE INVENTION. Before adding any new literal
// here: grep -inE "<code-or-number>"
// /root/aria/brain/01-Projekte/supplierpulse-multi-qaf/10-analyse-*.md must
// be EMPTY. The wide-slot/multi-row-header/split-column IDENTITY grid
// layouts below are lifted verbatim (same cell coordinates, same invented
// content) from header-parser.test.ts's own buildWideSlotGrid/
// buildMultiRowHeaderGrid/buildSplitColumnGrid — reused rather than
// reinvented so this file's fixtures are provably the SAME already-reviewed
// structural patterns, translated from plain grids into real ExcelJS
// worksheets (this module's own entry points need real Worksheet objects,
// unlike header-parser.ts's own pure-grid unit tests).
//
// Covers all 4 structural patterns (task instruction): wide-slot + shared
// manufacturing profile (Pattern A, MX-like — 10-analyse-mx.md), multi-row
// header with no slot-index row (Pattern B, NCAR-like —
// 10-analyse-ncar.md), split-column orphaned-but-real variants (Pattern C,
// ClarWE-like — 10-analyse-clarwe-eu.md), and the MATERIAL-sheet variant-
// identity fallback (Pattern D, NAFTA-like — 10-analyse-nafta.md, the exact
// case KAR-934's own real-file regression names).

import { describe, it, expect } from 'vitest'
import ExcelJS from 'exceljs'
import { columnIndexToLetter } from '../types'
import { DEFAULT_COL_FROM } from '../header-parser'
import type { MultiQafDetectionResult } from '../../qaf-type-detector'
import { serializeMultiQafContainer, deserializeMultiQafContainer } from '../serialization'
import {
  assembleMultiQafContainer,
  generateVirtualVariants,
  resolveMultiQafContainerIngest,
  reviewStatusForMultiQafContainer,
} from '../container-assembly'

function col(col0: number): string {
  return columnIndexToLetter(col0 + 1)
}

const CONFIRMED: MultiQafDetectionResult = {
  classification: 'confirmed_multi_qaf',
  confidence: 0.9,
  signals: [],
  matchedVersionMarker: null,
}

// ── Shared identity-block builder (wide-slot pattern — mirrors header-
// parser.test.ts's buildWideSlotGrid rows 2-7 exactly) ─────────────────────

function applyWideSlotIdentityBlock(ws: ExcelJS.Worksheet): { active: number[]; inactive: number[] } {
  const Q = DEFAULT_COL_FROM
  const activeCols = Array.from({ length: 10 }, (_, i) => Q + i) // Q..Z
  const inactiveCols = [Q + 15, Q + 16] // AF, AG
  const reservedCols = Array.from({ length: 13 }, (_, i) => Q + 17 + i) // AH..AT

  ws.getCell('P2').value = 'Slot-Index'
  ws.getCell('P3').value = 'BMW Variant' // allow-customer-string
  ws.getCell('P4').value = 'Antrieb'
  ws.getCell('P5').value = 'Shape'
  ws.getCell('P6').value = 'LL/RL'
  ws.getCell('P7').value = 'Stückzahl Ø/Jahr'

  activeCols.forEach((c, i) => {
    ws.getCell(`${col(c)}2`).value = i + 1
    ws.getCell(`${col(c)}3`).value = `slot-${i + 1}`
    ws.getCell(`${col(c)}4`).value = 'awd'
    ws.getCell(`${col(c)}5`).value = i < 5 ? 'l-shape' : 'i-shape'
    ws.getCell(`${col(c)}6`).value = i % 2 === 0 ? 'll' : 'rl'
    ws.getCell(`${col(c)}7`).value = 1000 * (i + 1)
  })
  inactiveCols.forEach((c, i) => {
    ws.getCell(`${col(c)}2`).value = 11 + i
    ws.getCell(`${col(c)}3`).value = `slot-${11 + i}`
    ws.getCell(`${col(c)}4`).value = 'awd'
    ws.getCell(`${col(c)}5`).value = 'i-shape'
    ws.getCell(`${col(c)}6`).value = 'll'
    ws.getCell(`${col(c)}7`).value = 0 // identity present, zero volume -> inactive.
  })
  reservedCols.forEach((c, i) => {
    ws.getCell(`${col(c)}2`).value = 13 + i // index only -> reserved.
  })

  return { active: activeCols, inactive: inactiveCols }
}

// ── Pattern A: wide-slot + shared manufacturing profile (MX-like) ─────────

function buildPatternAWorkbook(): ExcelJS.Workbook {
  const wb = new ExcelJS.Workbook()
  const summary = wb.addWorksheet('Zusammenfassung')
  const material = wb.addWorksheet('Material')
  const manufacturing = wb.addWorksheet('Fertigungskosten')

  const { active, inactive } = applyWideSlotIdentityBlock(summary)
  const allIdentityCols = [...active, ...inactive]

  // Fertigungskosten row on Summary (row 20 — clear of the identity block's
  // own rows 2-7) — every identity-bearing column binds via a hardcoded
  // absolute cellReference to the SAME manufacturing-profile Total cell
  // (module header (d) — "variant-independent Fertigungskosten" real
  // pattern, also exercised standalone in Pattern D below).
  summary.getCell('B20').value = 'Fertigungskosten'
  allIdentityCols.forEach((c) => {
    summary.getCell(`${col(c)}20`).value = { formula: 'Fertigungskosten!$U$10', result: 46.5 } as ExcelJS.CellValue
  })

  // KAR-951 — the Summary-money rows container-assembly.ts's new extraction
  // step (locateSummaryMoneyRow/extractSummaryMoneyRow, profile-parser.ts)
  // label-anchors: rows 21/22/23/24/25, clear of the identity block (rows
  // 2-7) and the Fertigungskosten row (20). Real-file evidence for the
  // 'Ausschusskosten'/'SUMME Overhead' labels: KAR-951 livetest module header
  // (summary-totals-differ.ts). Row 25 ('ANGEBOTSBASISPREIS inkl. Umlage') is
  // the KAR-951 F1 adversarial-review fix's own addition — a DIFFERENT row
  // from row 23's plain 'ANGEBOTSBASISPREIS', deliberately given its own
  // distinct values below so a test bug conflating the two would be caught.
  summary.getCell('B21').value = 'Ausschusskosten'
  summary.getCell('B22').value = 'SUMME Overhead'
  summary.getCell('B23').value = 'ANGEBOTSBASISPREIS'
  summary.getCell('B24').value = 'ANGEBOTSPREIS'
  summary.getCell('B25').value = 'ANGEBOTSBASISPREIS inkl. Umlage'
  allIdentityCols.forEach((c, i) => {
    summary.getCell(`${col(c)}21`).value = 0.5 + i * 0.1
    summary.getCell(`${col(c)}22`).value = 1.5 + i * 0.1
    summary.getCell(`${col(c)}23`).value = 50 + i
    summary.getCell(`${col(c)}24`).value = 55 + i
    summary.getCell(`${col(c)}25`).value = 60 + i
  })

  // Material sheet: SAME identity block (so stableInternalId aligns across
  // sheets — identity.ts's own determinism guarantee, see container-
  // assembly.ts module header "Known scope limits") + a minimal 2-row
  // material matrix with a per-column SUMPRODUCT total.
  applyWideSlotIdentityBlock(material)
  material.getCell('A1').value = 'Position Number'
  material.getCell('B1').value = 'Teilebezeichnung'
  material.getCell('C1').value = 'Angebotswährung'
  material.getCell('D1').value = 'Materialkosten pro Mengeneinheit (AW)'
  material.getCell('A8').value = 'P1'
  material.getCell('B8').value = 'Bauteil 1'
  material.getCell('C8').value = 'EUR'
  material.getCell('D8').value = 2.5
  material.getCell('A9').value = 'P2'
  material.getCell('B9').value = 'Bauteil 2'
  material.getCell('C9').value = 'EUR'
  material.getCell('D9').value = 1.2
  allIdentityCols.forEach((c) => {
    material.getCell(`${col(c)}8`).value = 2
    material.getCell(`${col(c)}9`).value = 3
    material.getCell(`${col(c)}10`).value = {
      formula: `SUMPRODUCT($D$8:$D$9,${col(c)}8:${col(c)}9)`,
      result: 2.5 * 2 + 1.2 * 3,
    } as ExcelJS.CellValue
  })

  manufacturing.getCell('B10').value = 'Manufacturing Total'
  manufacturing.getCell('U10').value = { formula: 'SUMIF(A1:A5,">0",B1:B5)', result: 46.5 } as ExcelJS.CellValue

  return wb
}

// ── Pattern B: multi-row header, no slot-index row (NCAR-like) ────────────

function buildPatternBWorkbook(): ExcelJS.Workbook {
  const wb = new ExcelJS.Workbook()
  const summary = wb.addWorksheet('Zusammenfassung')
  const Q = DEFAULT_COL_FROM
  const specs = [
    { code: 'v201-lc1', motor: 'eng-a', axle: 'ax-n', steer: 'll', peak: 84000 },
    { code: 'v202-lc1', motor: 'eng-a', axle: 'ax-n', steer: 'rl', peak: 58500 },
    { code: 'v203-lc1', motor: 'eng-a', axle: 'ax-s', steer: 'll', peak: 96000 },
    { code: 'v204-lc1', motor: 'eng-a', axle: 'ax-s', steer: 'rl', peak: 31000 },
    { code: 'v205-lc2', motor: 'eng-b', axle: 'ax-t', steer: 'll', peak: 14000 },
    { code: 'v206-lc2', motor: 'eng-b', axle: 'ax-t', steer: 'rl', peak: 2200 },
    { code: 'v207-lc2', motor: 'eng-b', axle: 'ax-u', steer: 'll', peak: 17500 },
    { code: 'v208-lc2', motor: 'eng-b', axle: 'ax-u', steer: 'rl', peak: 6100 },
  ]
  summary.getCell('A5').value = 'BMW Variant ID' // allow-customer-string
  summary.getCell('A6').value = 'Motor'
  summary.getCell('A7').value = 'Achscode'
  summary.getCell('A9').value = 'LL/RL'
  summary.getCell('A15').value = 'Stückzahl peak'
  specs.forEach((s, i) => {
    const c = Q + i
    summary.getCell(`${col(c)}5`).value = s.code
    summary.getCell(`${col(c)}6`).value = s.motor
    summary.getCell(`${col(c)}7`).value = s.axle
    summary.getCell(`${col(c)}9`).value = s.steer
    summary.getCell(`${col(c)}15`).value = s.peak
  })
  return wb
}

// ── Pattern C: split-column, orphaned-but-real variants (ClarWE-like) ─────

function buildPatternCWorkbook(): ExcelJS.Workbook {
  const wb = new ExcelJS.Workbook()
  const summary = wb.addWorksheet('Zusammenfassung')
  const material = wb.addWorksheet('BOM Detail EU')
  const Q = DEFAULT_COL_FROM

  function applySplitColumnBlock(ws: ExcelJS.Worksheet): number[] {
    ws.getCell('P2').value = 'Slot-Index'
    ws.getCell('P3').value = 'Variantencode'
    ws.getCell('P4').value = 'Fahrzeug'
    ws.getCell('P5').value = 'LL/RL'
    ws.getCell('P6').value = 'Stückzahl lifetime'
    const cols: number[] = []
    for (let i = 0; i < 6; i++) {
      const c = Q + i
      cols.push(c)
      ws.getCell(`${col(c)}2`).value = i + 1
      ws.getCell(`${col(c)}3`).value = `code-${i + 1}`
      ws.getCell(`${col(c)}4`).value = 'wx1'
      ws.getCell(`${col(c)}5`).value = i % 2 === 0 ? 'll' : 'rl'
      ws.getCell(`${col(c)}6`).value = 1000 + i * 500
    }
    const orphanedCols = [Q + 8, Q + 9]
    orphanedCols.forEach((c, i) => {
      cols.push(c)
      ws.getCell(`${col(c)}3`).value = `other-code-${i + 1}`
      ws.getCell(`${col(c)}4`).value = 'other-programme'
      ws.getCell(`${col(c)}5`).value = i === 0 ? 'll' : 'rl'
      ws.getCell(`${col(c)}6`).value = 220000 + i * 40000
    })
    return cols
  }

  applySplitColumnBlock(summary)
  const materialCols = applySplitColumnBlock(material)

  material.getCell('A1').value = 'Position Number'
  material.getCell('B1').value = 'Teilebezeichnung'
  material.getCell('C1').value = 'Angebotswährung'
  material.getCell('D1').value = 'Materialkosten pro Mengeneinheit (AW)'
  material.getCell('A10').value = 'P1'
  material.getCell('B10').value = 'Bauteil 1'
  material.getCell('C10').value = 'EUR'
  material.getCell('D10').value = 4
  materialCols.forEach((c) => {
    material.getCell(`${col(c)}10`).value = 1
  })

  return wb
}

// ── Pattern D: Summary yields ZERO variants, identity lives on MATERIAL
// (NAFTA-like — the exact case KAR-934's own real-file regression names) ──

function buildPatternDWorkbook(): ExcelJS.Workbook {
  const wb = new ExcelJS.Workbook()
  const summary = wb.addWorksheet('Zusammenfassung')
  const material = wb.addWorksheet('Material')

  // Summary carries a Fertigungskosten label (so it LOOKS like a normal
  // Summary sheet structurally) but NO dimension/slot-index rows at all —
  // header-parser.ts's own parseVariantHeaderBlock must materialize zero
  // variants here, forcing the MATERIAL-sheet fallback.
  summary.getCell('B20').value = 'Fertigungskosten'

  applyWideSlotIdentityBlock(material)

  return wb
}

// ── Pattern E: single variant, material rows mix currencies (KAR-935
// adversarial review F1) ────────────────────────────────────────────────

function applySingleVariantIdentityBlock(ws: ExcelJS.Worksheet, col0: number): void {
  ws.getCell('P2').value = 'Slot-Index'
  ws.getCell('P3').value = 'BMW Variant' // allow-customer-string
  ws.getCell('P4').value = 'Antrieb'
  ws.getCell('P5').value = 'Shape'
  ws.getCell('P6').value = 'LL/RL'
  ws.getCell('P7').value = 'Stückzahl Ø/Jahr'
  ws.getCell(`${col(col0)}2`).value = 1
  ws.getCell(`${col(col0)}3`).value = 'slot-1'
  ws.getCell(`${col(col0)}4`).value = 'awd'
  ws.getCell(`${col(col0)}5`).value = 'l-shape'
  ws.getCell(`${col(col0)}6`).value = 'll'
  ws.getCell(`${col(col0)}7`).value = 1000
}

function buildPatternEWorkbook(): ExcelJS.Workbook {
  const wb = new ExcelJS.Workbook()
  const summary = wb.addWorksheet('Zusammenfassung')
  const material = wb.addWorksheet('Material')
  const Q = DEFAULT_COL_FROM

  // Same single identity on both sheets (no F2 collision/matching concern
  // here — this fixture isolates F1 alone) — one variant, two material rows
  // priced in two DIFFERENT currencies (EUR/USD), the anticipated shape
  // types.ts's own MultiQafContainer.currencies doc names ("a Multi-QAF
  // container is NOT guaranteed single-currency").
  applySingleVariantIdentityBlock(summary, Q)
  applySingleVariantIdentityBlock(material, Q)

  material.getCell('A1').value = 'Position Number'
  material.getCell('B1').value = 'Teilebezeichnung'
  material.getCell('C1').value = 'Angebotswährung'
  material.getCell('D1').value = 'Materialkosten pro Mengeneinheit (AW)'
  material.getCell('A8').value = 'P1'
  material.getCell('B8').value = 'Bauteil 1'
  material.getCell('C8').value = 'EUR'
  material.getCell('D8').value = 5
  material.getCell(`${col(Q)}8`).value = 2
  material.getCell('A9').value = 'P2'
  material.getCell('B9').value = 'Bauteil 2'
  material.getCell('C9').value = 'USD'
  material.getCell('D9').value = 3
  material.getCell(`${col(Q)}9`).value = 4

  return wb
}

// ── Pattern F: two Summary variants collide on identical dimension values
// (raw canonical-key collision), but the Material sheet's OWN independent
// header re-parse only materializes ONE of the two colliding columns — the
// exact real-file shape container-assembly.ts's own module header documents
// ("the Material sheet's own header-block re-parse can find a different
// variant subset... than the Summary sheet's own") and KAR-935 adversarial
// review F2 names ─────────────────────────────────────────────────────────

function buildPatternFWorkbook(): ExcelJS.Workbook {
  const wb = new ExcelJS.Workbook()
  const summary = wb.addWorksheet('Zusammenfassung')
  const material = wb.addWorksheet('Material')
  const Q = DEFAULT_COL_FROM
  const R = Q + 1

  summary.getCell('P2').value = 'Slot-Index'
  summary.getCell('P3').value = 'BMW Variant' // allow-customer-string
  summary.getCell('P4').value = 'Antrieb'
  summary.getCell('P5').value = 'Shape'
  summary.getCell('P6').value = 'LL/RL'
  summary.getCell('P7').value = 'Stückzahl Ø/Jahr'
  ;[Q, R].forEach((c, i) => {
    summary.getCell(`${col(c)}2`).value = i + 1
    summary.getCell(`${col(c)}3`).value = `slot-${i + 1}`
    summary.getCell(`${col(c)}4`).value = 'awd'
    summary.getCell(`${col(c)}5`).value = 'l-shape'
    summary.getCell(`${col(c)}6`).value = 'll'
    summary.getCell(`${col(c)}7`).value = 1000 * (i + 1)
  })

  // Material sheet only ever saw the slot-1 column (slot-2's own data never
  // materialized on this sheet's own re-parse).
  applySingleVariantIdentityBlock(material, Q)

  material.getCell('A1').value = 'Position Number'
  material.getCell('B1').value = 'Teilebezeichnung'
  material.getCell('C1').value = 'Angebotswährung'
  material.getCell('D1').value = 'Materialkosten pro Mengeneinheit (AW)'
  material.getCell('A8').value = 'P1'
  material.getCell('B8').value = 'Bauteil 1'
  material.getCell('C8').value = 'EUR'
  material.getCell('D8').value = 5
  material.getCell(`${col(Q)}8`).value = 2

  return wb
}

// ── Pattern G: the Material sheet's own variant has dimension values that
// never occur on the Summary sheet at all — an UNMATCHABLE material variant
// (KAR-935 F2 "never guess, warn + exclude" branch) ────────────────────────

function buildPatternGWorkbook(): ExcelJS.Workbook {
  const wb = new ExcelJS.Workbook()
  const summary = wb.addWorksheet('Zusammenfassung')
  const material = wb.addWorksheet('Material')
  const Q = DEFAULT_COL_FROM

  applySingleVariantIdentityBlock(summary, Q)

  material.getCell('P2').value = 'Slot-Index'
  material.getCell('P3').value = 'BMW Variant' // allow-customer-string
  material.getCell('P4').value = 'Antrieb'
  material.getCell('P5').value = 'Shape'
  material.getCell('P6').value = 'LL/RL'
  material.getCell('P7').value = 'Stückzahl Ø/Jahr'
  material.getCell(`${col(Q)}2`).value = 9
  material.getCell(`${col(Q)}3`).value = 'slot-9'
  material.getCell(`${col(Q)}4`).value = 'rwd' // different drive -> different raw key, never occurs on Summary.
  material.getCell(`${col(Q)}5`).value = 'i-shape'
  material.getCell(`${col(Q)}6`).value = 'rl'
  material.getCell(`${col(Q)}7`).value = 500

  material.getCell('A1').value = 'Position Number'
  material.getCell('B1').value = 'Teilebezeichnung'
  material.getCell('C1').value = 'Angebotswährung'
  material.getCell('D1').value = 'Materialkosten pro Mengeneinheit (AW)'
  material.getCell('A8').value = 'P1'
  material.getCell('B8').value = 'Bauteil 1'
  material.getCell('C8').value = 'EUR'
  material.getCell('D8').value = 5
  material.getCell(`${col(Q)}8`).value = 2

  return wb
}

// ── Pattern H: KAR-936 fuzzy-fallback financial-aggregation gate
// (adversarial-review-of-#308 F1/F6) — Summary and Material each materialize
// 2 known dimensions, but only ONE overlaps between the two sheets, so the
// exact-RAW-key pass misses (asymmetric dimension sets) and the core_subset_
// exact fallback candidate rests on a SINGLE shared, informative dimension
// (`steeringSide`) — exactly the "a Single-Dimension-Match reicht" failure
// scenario the review named. The gate (FUZZY_AGGREGATION_MIN_DIMENSIONS/
// FUZZY_AGGREGATION_MIN_CONFIDENCE, container-assembly.ts) must reject this:
// no stableInternalId translation, no material total — instead of silently
// moving real cost data to a variant it may not belong to. ─────────────────

function buildPatternHRejectedWorkbook(): ExcelJS.Workbook {
  const wb = new ExcelJS.Workbook()
  const summary = wb.addWorksheet('Zusammenfassung')
  const material = wb.addWorksheet('Material')
  const Q = DEFAULT_COL_FROM

  summary.getCell('P2').value = 'Slot-Index'
  summary.getCell('P3').value = 'BMW Variant' // allow-customer-string
  summary.getCell('P4').value = 'Antrieb'
  summary.getCell('P5').value = 'LL/RL'
  summary.getCell('P6').value = 'Stückzahl Ø/Jahr'
  summary.getCell(`${col(Q)}2`).value = 1
  summary.getCell(`${col(Q)}3`).value = 'slot-1'
  summary.getCell(`${col(Q)}4`).value = 'awd'
  summary.getCell(`${col(Q)}5`).value = 'll'
  summary.getCell(`${col(Q)}6`).value = 1000

  // Material's own re-parse only recognizes LL/RL (shared with Summary) +
  // Motor (its own, absent on Summary) — the two sheets' dimension sets
  // overlap in exactly ONE informative key.
  material.getCell('P2').value = 'Slot-Index'
  material.getCell('P3').value = 'BMW Variant' // allow-customer-string
  material.getCell('P4').value = 'LL/RL'
  material.getCell('P5').value = 'Motor'
  material.getCell('P6').value = 'Stückzahl Ø/Jahr'
  material.getCell(`${col(Q)}2`).value = 1
  material.getCell(`${col(Q)}3`).value = 'slot-1-mat'
  material.getCell(`${col(Q)}4`).value = 'll'
  material.getCell(`${col(Q)}5`).value = 'm1'
  material.getCell(`${col(Q)}6`).value = 1000

  material.getCell('A1').value = 'Position Number'
  material.getCell('B1').value = 'Teilebezeichnung'
  material.getCell('C1').value = 'Angebotswährung'
  material.getCell('D1').value = 'Materialkosten pro Mengeneinheit (AW)'
  material.getCell('A20').value = 'P1'
  material.getCell('B20').value = 'Bauteil 1'
  material.getCell('C20').value = 'EUR'
  material.getCell('D20').value = 5
  material.getCell(`${col(Q)}20`).value = 2

  return wb
}

// The positive counterpart (adversarial-review-of-#308 F6 — a non-gated
// correct-PAIRING assertion, not just a count): 2 Summary/2 Material
// variants sharing enough (6) known dimensions with only ONE Summary-only
// extra ("Shape", unrecognized label) to force the exact-RAW-key pass to
// miss while clearing the gate comfortably (coverage 6/7, confidence
// ~0.857). Each variant gets a DIFFERENT material unit-cost factor, so a
// swapped pairing (variant A's data landing on variant B) is directly
// observable, not just a non-zero count.

function buildPatternHAcceptedWorkbook(): ExcelJS.Workbook {
  const wb = new ExcelJS.Workbook()
  const summary = wb.addWorksheet('Zusammenfassung')
  const material = wb.addWorksheet('Material')
  const Q = DEFAULT_COL_FROM
  const R = Q + 1

  function identityRows(ws: ExcelJS.Worksheet, includeShape: boolean): void {
    ws.getCell('P2').value = 'Slot-Index'
    ws.getCell('P3').value = 'BMW Variant' // allow-customer-string
    ws.getCell('P4').value = 'Fahrzeug'
    ws.getCell('P5').value = 'Plattform'
    ws.getCell('P6').value = 'Antrieb'
    ws.getCell('P7').value = 'LL/RL'
    ws.getCell('P8').value = 'Motor'
    ws.getCell('P9').value = 'Rack-Typ'
    ws.getCell('P10').value = 'Stückzahl Ø/Jahr'
    const vals = [
      { slot: 1, code: 'slot-1', vehicle: 'q71', platform: 'mqb', drive: 'awd', steer: 'll', motor: 'm1', rack: 'r1' },
      { slot: 2, code: 'slot-2', vehicle: 'x99', platform: 'zpl', drive: 'fwd', steer: 'rl', motor: 'mz9', rack: 'rz9' },
    ]
    ;[Q, R].forEach((c, i) => {
      const v = vals[i]!
      ws.getCell(`${col(c)}2`).value = v.slot
      ws.getCell(`${col(c)}3`).value = v.code
      ws.getCell(`${col(c)}4`).value = v.vehicle
      ws.getCell(`${col(c)}5`).value = v.platform
      ws.getCell(`${col(c)}6`).value = v.drive
      ws.getCell(`${col(c)}7`).value = v.steer
      ws.getCell(`${col(c)}8`).value = v.motor
      ws.getCell(`${col(c)}9`).value = v.rack
      ws.getCell(`${col(c)}10`).value = 1000
    })
    if (includeShape) {
      ws.getCell('P11').value = 'Shape'
      ws.getCell(`${col(Q)}11`).value = 'l-shape'
      ws.getCell(`${col(R)}11`).value = 'i-shape'
    }
  }

  identityRows(summary, true)
  identityRows(material, false)

  material.getCell('A1').value = 'Position Number'
  material.getCell('B1').value = 'Teilebezeichnung'
  material.getCell('C1').value = 'Angebotswährung'
  material.getCell('D1').value = 'Materialkosten pro Mengeneinheit (AW)'
  material.getCell('A20').value = 'P1'
  material.getCell('B20').value = 'Bauteil 1'
  material.getCell('C20').value = 'EUR'
  material.getCell('D20').value = 5
  material.getCell(`${col(Q)}20`).value = 2 // variant 1 factor -> 5*2=10
  material.getCell(`${col(R)}20`).value = 3 // variant 2 factor -> 5*3=15

  return wb
}

describe('assembleMultiQafContainer — Pattern H (KAR-936 fuzzy-fallback financial-aggregation gate, adversarial review of #308 F1/F6)', () => {
  it('REJECTS a single-shared-dimension fuzzy candidate — no stableInternalId translation, no material total, below-gate warning instead', async () => {
    const wb = buildPatternHRejectedWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)

    expect(container.activeVariants).toHaveLength(1)
    const belowGate = container.warnings.find((w) => w.code === 'material_variant_fuzzy_match_below_aggregation_gate')
    expect(belowGate).toBeDefined()
    expect(belowGate!.reviewRelevant).toBe(true)
    expect(belowGate!.message).toContain('1 gemeinsame Dimension')
    // No silent translation happened — this must still be reported exactly
    // like an unresolved exact-key miss (the gate's own doc comment).
    expect(container.warnings.some((w) => w.code === 'material_variant_fuzzy_matched_to_summary')).toBe(false)
    expect(container.warnings.some((w) => w.code === 'material_variant_unmatched_to_summary')).toBe(true)

    const [virtual] = generateVirtualVariants(container)
    expect(virtual!.materialRows).toHaveLength(0)
    expect(virtual!.summaryTotals.materialCosts).toBeNull()
  })

  it('ACCEPTS a 6-of-7-dimension fuzzy candidate (confidence ~0.857) and translates each Material column to the CORRECT container variant — verified by distinct per-variant cost values, not just a non-zero count', async () => {
    const wb = buildPatternHAcceptedWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)

    expect(container.activeVariants).toHaveLength(2)
    const fuzzyMatched = container.warnings.filter((w) => w.code === 'material_variant_fuzzy_matched_to_summary')
    expect(fuzzyMatched).toHaveLength(2)
    expect(container.warnings.some((w) => w.code === 'material_variant_fuzzy_match_below_aggregation_gate')).toBe(false)
    expect(container.warnings.some((w) => w.code === 'material_variant_unmatched_to_summary')).toBe(false)

    const virtualVariants = generateVirtualVariants(container)
    const slot1 = virtualVariants.find((v) => v.definition.originalVariantNumber === 'slot-1')!
    const slot2 = virtualVariants.find((v) => v.definition.originalVariantNumber === 'slot-2')!

    // The correctness-critical assertion (F6): each variant's material total
    // reflects ITS OWN factor, never the other variant's — a swapped
    // pairing would show 15/10 instead of 10/15.
    expect(slot1.summaryTotals.materialCosts).toEqual({ value: 10, currency: 'EUR' })
    expect(slot2.summaryTotals.materialCosts).toEqual({ value: 15, currency: 'EUR' })
  })
})

// ── Tests ────────────────────────────────────────────────────────────────

describe('assembleMultiQafContainer — Pattern A (wide-slot + shared manufacturing profile)', () => {
  it('assembles active/inactive variants, material matrix, profile binding, and produces consistent VirtualQafVariants', async () => {
    const wb = buildPatternAWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED, { fileName: 'pattern-a.xlsx', fileHash: 'hash-a' })

    expect(container.activeVariants).toHaveLength(10)
    // inactiveVariants bucket = inactive(2) + reserved(13), per types.ts doc.
    expect(container.inactiveVariants).toHaveLength(15)
    expect(container.sharedMaterialMaster.length).toBeGreaterThan(0)
    expect(container.sharedManufacturingProfiles.length).toBeGreaterThan(0)
    expect(container.variantProfileBindings.length).toBeGreaterThan(0)
    expect(container.variantProfileBindings.every((b) => b.bindingEvidence.kind === 'cellReference')).toBe(true)
    expect(container.templateFingerprint.family).not.toBe('unknown')
    expect(container.templateFingerprint.classification).not.toBeNull()
    // This fixture's own dimension values are deliberately narrow (same
    // shape header-parser.test.ts's own wide-slot fixture uses — 4 distinct
    // shape/steering combinations across 10 active variants), so a genuine
    // compositeCanonicalKey collision fires here — assembleMultiQafContainer
    // must disambiguate it so `stableInternalId` (the key materialRows/
    // bindings are actually keyed by) stays unique per variant; an
    // undetected collision would make two genuinely different variants
    // silently share one VirtualQafVariant's material/profile data
    // (identity.ts's own "aggregation" risk, applied to this module's own
    // join key).
    expect(container.warnings.some((w) => w.code === 'canonical_key_collision')).toBe(true)
    const stableIds = container.activeVariants.map((v) => v.stableInternalId)
    expect(new Set(stableIds).size).toBe(stableIds.length)
    // KAR-935 adversarial-review F4 fix: a container carrying a genuine
    // canonical-key collision is review-worthy.
    expect(reviewStatusForMultiQafContainer(container)).toBe('review_required')

    // KAR-951 — Summary-money-row extraction: fail-closed row-locating (never
    // a fixed row number, see profile-parser.ts locateSummaryMoneyRow) finds
    // the 5 rows this fixture added and reads a real per-variant value +
    // source cell, replacing the old hardcoded `null`.
    expect(Object.keys(container.summaryMoneyRowsByVariant).length).toBeGreaterThan(0)
    for (const definition of container.activeVariants) {
      const rows = container.summaryMoneyRowsByVariant[definition.stableInternalId]
      expect(rows?.scrap?.amount.value).not.toBeNull()
      expect(rows?.otherSurcharges?.amount.value).not.toBeNull()
      expect(rows?.offerBasePrice?.amount.value).not.toBeNull()
      expect(rows?.offerPrice?.amount.value).not.toBeNull()
      expect(rows?.scrap?.sourceCell.sheet).toBe('Zusammenfassung')
      // KAR-951 F1 fix — row 25 ('ANGEBOTSBASISPREIS inkl. Umlage') is its
      // OWN row, distinct from row 23's offerBasePrice (50+i vs. 60+i).
      expect(rows?.offerBasePriceInclAllocation?.amount.value).not.toBeNull()
      expect(rows?.offerBasePriceInclAllocation?.amount.value).not.toBe(rows?.offerBasePrice?.amount.value)
    }

    const virtualVariants = generateVirtualVariants(container)
    const activeVirtual = virtualVariants.filter((v) => v.definition.activeState === 'active')
    expect(activeVirtual).toHaveLength(10)
    // KAR-935 adversarial-review F5 fix: the collision warning must reach at
    // least one of the colliding variants' own `.warnings` — before the fix,
    // `variantWarningsFor`'s string-prefix-only match never attached it to
    // ANY variant (identity.ts's own collision message never starts with
    // "Variante ${id}: ").
    expect(activeVirtual.some((v) => v.warnings.some((w) => w.code === 'canonical_key_collision'))).toBe(true)
    for (const v of activeVirtual) {
      // Material total consistency: Σ(unitCost × factor) over the variant's
      // own materialRows must equal summaryTotals.materialCosts.
      const expectedMaterial = v.materialRows.reduce((sum, r) => sum + (r.effectiveCost.value ?? 0), 0)
      expect(v.summaryTotals.materialCosts?.value).toBeCloseTo(expectedMaterial, 6)
      expect(v.summaryTotals.materialCosts?.value).toBeCloseTo(2.5 * 2 + 1.2 * 3, 6)
      // Manufacturing cost consistency: bound profile's own cached total.
      expect(v.summaryTotals.manufacturingCosts?.value).toBeCloseTo(46.5, 6)
      expect(v.selectedManufacturingProfile).not.toBeNull()
      // KAR-951 — scrap/otherSurcharges/offerBasePrice/offerPrice used to be
      // hardcoded `null` (see the module header this PR's own diff removes)
      // — now real values from the fixture's own rows 21-24.
      expect(v.summaryTotals.scrap).not.toBeNull()
      expect(v.summaryTotals.otherSurcharges).not.toBeNull()
      expect(v.summaryTotals.offerBasePrice).not.toBeNull()
      expect(v.summaryTotals.offerPrice).not.toBeNull()
      // KAR-951 F1 fix — offerBasePriceInclAllocation (row 25) wired through
      // generateVirtualVariants the same way, never left at the old
      // hardcoded `null`.
      expect(v.summaryTotals.offerBasePriceInclAllocation).not.toBeNull()
      expect(v.summaryTotals.totalProductionCosts?.value).toBeCloseTo(expectedMaterial + 46.5, 6)
    }
  })

  it('round-trips through serialize/deserialize byte-for-byte (JSON-shape-lossless)', async () => {
    const wb = buildPatternAWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)
    const roundTripped = deserializeMultiQafContainer(serializeMultiQafContainer(container))
    expect(roundTripped).toEqual(container)
  })
})

describe('assembleMultiQafContainer — Pattern B (multi-row header, no slot-index row)', () => {
  it('materializes 8 active variants purely from dimension rows, no Material sheet -> empty matrix with an honest warning', async () => {
    const wb = buildPatternBWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)

    expect(container.activeVariants).toHaveLength(8)
    expect(container.inactiveVariants).toHaveLength(0)
    expect(container.sharedMaterialMaster).toHaveLength(0)
    expect(container.warnings.some((w) => w.code === 'material_sheet_not_found')).toBe(true)
    expect(container.warnings.some((w) => w.code === 'manufacturing_summary_row_not_found')).toBe(true)
    expect(container.variantProfileBindings).toHaveLength(0)
    // KAR-935 F4 fix: this fixture's 8 specs are pairwise distinct on every
    // extracted dimension (motor × axle × steer) — no collision, no
    // unmatched material variant, no ambiguous binding -> 'ok'.
    expect(reviewStatusForMultiQafContainer(container)).toBe('ok')

    const virtualVariants = generateVirtualVariants(container)
    expect(virtualVariants).toHaveLength(8)
    for (const v of virtualVariants) {
      expect(v.materialRows).toHaveLength(0)
      expect(v.summaryTotals.materialCosts).toBeNull()
      expect(v.summaryTotals.manufacturingCosts).toBeNull()
      expect(v.summaryTotals.totalProductionCosts).toBeNull()
    }
  })
})

describe('assembleMultiQafContainer — Pattern C (split-column, orphaned-but-real variants)', () => {
  it('materializes 6 slot-indexed + 2 orphaned-but-real variants (8 total, all active), with a material matrix', async () => {
    const wb = buildPatternCWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)

    expect(container.activeVariants).toHaveLength(8)
    expect(container.inactiveVariants).toHaveLength(0)
    expect(container.sharedMaterialMaster.length).toBeGreaterThan(0)

    const virtualVariants = generateVirtualVariants(container)
    expect(virtualVariants).toHaveLength(8)
    for (const v of virtualVariants) {
      expect(v.materialRows.length).toBeGreaterThan(0)
      expect(v.summaryTotals.materialCosts?.value).toBeCloseTo(4, 6)
    }
  })
})

describe('assembleMultiQafContainer — Pattern D (MATERIAL-sheet variant-identity fallback, KAR-934 finding)', () => {
  it('falls back to the MATERIAL sheet when SUMMARY materializes zero variants, and skips profile binding honestly', async () => {
    const wb = buildPatternDWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)

    expect(container.activeVariants).toHaveLength(10)
    expect(container.inactiveVariants).toHaveLength(15)
    expect(container.variantProfileBindings).toHaveLength(0)
    expect(container.warnings.some((w) => w.code === 'variant_profile_binding_summary_columns_unavailable')).toBe(true)

    const virtualVariants = generateVirtualVariants(container)
    expect(virtualVariants.filter((v) => v.definition.activeState === 'active')).toHaveLength(10)
    // KAR-935 F4 fix: this fixture reuses applyWideSlotIdentityBlock's own
    // narrow dimension set (same as Pattern A), so it also produces a
    // genuine canonical-key collision — 'review_required', not 'ok' (see
    // Pattern B below for the clean, no-collision 'ok' case).
    expect(reviewStatusForMultiQafContainer(container)).toBe('review_required')
  })
})

describe('assembleMultiQafContainer — Pattern E (mixed-currency material costs, KAR-935 adversarial review F1)', () => {
  it('never fabricates a cross-currency materialCosts/totalProductionCosts total, exposes per-currency buckets, and warns', async () => {
    const wb = buildPatternEWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)

    expect(container.activeVariants).toHaveLength(1)
    expect(container.sharedMaterialMaster).toHaveLength(2)

    const [virtual] = generateVirtualVariants(container)
    expect(virtual!.materialRows).toHaveLength(2)
    // The old sumMoney() would have added 10 + 12 = 22 and labeled it with
    // whichever currency it found first (EUR) — a materially wrong total.
    // The fix leaves materialCosts (and the totalProductionCosts derived
    // from it) honestly absent instead.
    expect(virtual!.summaryTotals.materialCosts).toBeNull()
    expect(virtual!.summaryTotals.totalProductionCosts).toBeNull()
    expect(virtual!.summaryTotals.materialCostsByCurrency).toEqual(
      expect.arrayContaining([
        { value: 10, currency: 'EUR' }, // 5 * 2
        { value: 12, currency: 'USD' }, // 3 * 4
      ]),
    )
    expect(virtual!.summaryTotals.materialCostsByCurrency).toHaveLength(2)
    const mixedWarning = virtual!.warnings.find((w) => w.code === 'mixed_currency_material_costs')
    expect(mixedWarning).toBeDefined()
    expect(mixedWarning!.variantIds).toEqual([virtual!.variantId])
  })

  it('round-trips a mixed-currency container through serialize/deserialize losslessly', async () => {
    const wb = buildPatternEWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)
    const roundTripped = deserializeMultiQafContainer(serializeMultiQafContainer(container))
    expect(roundTripped).toEqual(container)
  })
})

describe('assembleMultiQafContainer — Pattern F (Material-sheet identity subset differs from Summary, KAR-935 adversarial review F2)', () => {
  it('translates the matched Material variant to the CONTAINER stableInternalId so its material row is not silently dropped', async () => {
    const wb = buildPatternFWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)

    expect(container.activeVariants).toHaveLength(2)
    expect(container.warnings.some((w) => w.code === 'canonical_key_collision')).toBe(true)
    // No unmatched-material-variant warning — the one Material column DID
    // match (slot 1).
    expect(container.warnings.some((w) => w.code === 'material_variant_unmatched_to_summary')).toBe(false)

    const virtualVariants = generateVirtualVariants(container)
    const slot1 = virtualVariants.find((v) => v.definition.originalVariantNumber === 'slot-1')!
    const slot2 = virtualVariants.find((v) => v.definition.originalVariantNumber === 'slot-2')!

    // The bug: under the old independent-disambiguation code, slot 1's
    // material row was keyed by the Material sheet's own UNSUFFIXED
    // stableInternalId (no collision on that sheet's own 1-member array),
    // which never matched slot 1's SUFFIXED container stableInternalId
    // (`...#dup-1`, from the Summary sheet's own 2-member collision) — the
    // row was silently treated as "not applicable" and materialCosts came
    // back null despite the real material data existing in the workbook.
    expect(slot1.materialRows).toHaveLength(1)
    expect(slot1.summaryTotals.materialCosts?.value).toBeCloseTo(10, 6) // 5 * 2

    // Slot 2 genuinely has no material column on this sheet — that IS a
    // legitimate miss, but because the matrix is otherwise populated (slot 1
    // has a row), it is flagged rather than silently treated the same as an
    // ordinary not-applicable cell.
    expect(slot2.materialRows).toHaveLength(0)
    expect(slot2.summaryTotals.materialCosts).toBeNull()
    expect(
      container.warnings.some((w) => w.code === 'material_matrix_variant_never_referenced' && w.variantIds?.includes(slot2.variantId)),
    ).toBe(true)
  })

  it('round-trips through serialize/deserialize losslessly', async () => {
    const wb = buildPatternFWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)
    const roundTripped = deserializeMultiQafContainer(serializeMultiQafContainer(container))
    expect(roundTripped).toEqual(container)
  })
})

describe('assembleMultiQafContainer — Pattern G (unmatchable Material-sheet variant, KAR-935 adversarial review F2)', () => {
  it('excludes the unmatched Material variant and warns instead of guessing an attribution', async () => {
    const wb = buildPatternGWorkbook()
    const container = await assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)

    expect(container.activeVariants).toHaveLength(1)
    // The material row itself is still extracted (row-level parsing does
    // not depend on variant-column matching — only the aggregate-row
    // CLASSIFICATION and per-column factor reading do, and both still see
    // the unmatched column, just keyed by ITS OWN (untranslated) id) — no
    // CONTAINER variant will ever look that id up.
    expect(container.sharedMaterialMaster).toHaveLength(1)
    const unmatched = container.warnings.find((w) => w.code === 'material_variant_unmatched_to_summary')
    expect(unmatched).toBeDefined()
    expect(unmatched!.reviewRelevant).toBe(true)
    expect(container.warnings.some((w) => w.code === 'material_matrix_no_matched_variant_identity')).toBe(true)
    // KAR-935 F4 fix: an unmatched material variant is review-worthy.
    expect(reviewStatusForMultiQafContainer(container)).toBe('review_required')

    const [virtual] = generateVirtualVariants(container)
    // The unmatched column's factor is keyed by its own local id, which
    // never matches this (the only) container variant's own stableInternalId
    // — so it correctly sees zero applicable material rows, never a guessed
    // attribution.
    expect(virtual!.materialRows).toHaveLength(0)
    expect(virtual!.summaryTotals.materialCosts).toBeNull()
    expect(container.sharedMaterialMaster[0]!.quantityFactorByVariant[virtual!.variantId]).toBeUndefined()
  })
})

describe('resolveMultiQafContainerIngest — fail-closed fallback', () => {
  const REJECTION = { reasonDe: 'Diese Datei ist vermutlich ein Multi-QAF.', reasonEn: 'This file is likely a Multi-QAF.' }

  it('returns ok:true with a container + virtual variants for a well-formed synthetic Multi-QAF workbook', async () => {
    const wb = buildPatternAWorkbook()
    const outcome = await resolveMultiQafContainerIngest({ worksheets: wb.worksheets }, CONFIRMED, REJECTION)
    expect(outcome.ok).toBe(true)
    if (outcome.ok) {
      expect(outcome.container.activeVariants.length).toBeGreaterThan(0)
      expect(outcome.virtualVariants.length).toBeGreaterThan(0)
    }
  })

  it('falls back to the original rejection message when assembly cannot locate any variant identity at all', async () => {
    const wb = new ExcelJS.Workbook()
    wb.addWorksheet('EmptySheet')
    const outcome = await resolveMultiQafContainerIngest({ worksheets: wb.worksheets }, CONFIRMED, REJECTION)
    expect(outcome.ok).toBe(false)
    if (!outcome.ok) {
      expect(outcome.reasonDe).toContain(REJECTION.reasonDe)
      expect(outcome.reasonEn).toContain(REJECTION.reasonEn)
    }
  })

  it('assembleMultiQafContainer itself throws for the same no-identity workbook (fail-closed, not a silent empty container)', async () => {
    const wb = new ExcelJS.Workbook()
    wb.addWorksheet('EmptySheet')
    await expect(assembleMultiQafContainer({ worksheets: wb.worksheets }, CONFIRMED)).rejects.toThrow()
  })
})
