// Unit tests for aggregate-impact.ts (KAR-944 / Multi-QAF-Programm P3.3).
//
// FIXTURE-DATEN-REGEL (same discipline as material-differ.test.ts's own
// header comment, KAR-929 adversarial review F4 — merge-blocker class):
// every code, number, label, and cell reference below is FREE INVENTION,
// not lifted from any real analyzed workbook.
//
// Integration-style: builds synthetic MultiQafContainer pairs and runs the
// REAL diffContainers/diffMaterial/diffProfiles/reconcileContainer functions
// to produce realistic inputs, then asserts on computeAggregateImpact's own
// gate/population/sign behavior — one scenario per Master-Prompt §19 gate
// (pass + fail), per-currency bucketing, volume-missing exclusion (per
// timeframe), sign-direction, baseline suppression + acknowledgment, and the
// always-available structural fallback. Real-file regression lives in
// aggregate-impact.real-files.test.ts (env-gated).

import { describe, expect, it } from 'vitest'
import { buildCompositeCanonicalKey, makeDimensionValue } from '../identity'
import type { MultiQafContainer, MultiQafWarning, SharedCostProfile, VariantDefinition, VariantDimensions, VariantMatrixRow, VariantProfileBinding } from '../types'
import { diffContainers } from '../container-differ'
import { diffMaterial } from '../material-differ'
import { diffProfiles } from '../profile-differ'
import { reconcileContainer } from '../variant-reconciliation'
import type { VariantMatchResult } from '../variant-matcher'
import { computeAggregateImpact } from '../aggregate-impact'
import { resolveVariantProfileBindings, type ProfileParserGridInput } from '../profile-parser'
import { gridFromCells } from '../../summary-parser'

// ── Minimal fixture builders (per-test-file local convention already
// established by material-differ.test.ts/profile-differ.test.ts/compare-
// flow.test.ts). ────────────────────────────────────────────────────────────

function dims(entries: Record<string, string>): VariantDimensions {
  const out: Record<string, ReturnType<typeof makeDimensionValue>> = {}
  for (const [k, v] of Object.entries(entries)) out[k] = makeDimensionValue(v)
  return out
}

function variant(id: string, overrides: Partial<VariantDefinition> = {}): VariantDefinition {
  const dimensions = overrides.dimensions ?? dims({ region: id })
  const labels = overrides.originalLabels ?? [`Label ${id}`]
  return {
    stableInternalId: id,
    originalColumn: overrides.originalColumn ?? 'Q',
    originalColumnIndex: overrides.originalColumnIndex ?? 17,
    originalVariantNumber: overrides.originalVariantNumber ?? '1',
    originalLabels: labels,
    normalizedLabels: overrides.normalizedLabels ?? labels.map((l) => l.toLowerCase()),
    compositeCanonicalKey: overrides.compositeCanonicalKey ?? buildCompositeCanonicalKey(dimensions, id),
    dimensions,
    annualVolume: overrides.annualVolume !== undefined ? overrides.annualVolume : 1000,
    peakVolume: overrides.peakVolume ?? null,
    lifetimeVolume: overrides.lifetimeVolume !== undefined ? overrides.lifetimeVolume : 5000,
    currency: overrides.currency !== undefined ? overrides.currency : 'EUR',
    activeState: overrides.activeState ?? 'active',
    sourceReferences: overrides.sourceReferences ?? [
      { sheet: 'Zusammenfassung', cell: `${overrides.originalColumn ?? 'Q'}2`, row: 1, column: overrides.originalColumnIndex ?? 17 },
    ],
    confidence: overrides.confidence ?? 0.9,
  }
}

function emptyContainer(overrides: Partial<MultiQafContainer> = {}): MultiQafContainer {
  return {
    sourceWorkbook: { fileName: null, fileHash: null, sheetNames: [], hiddenSheetNames: [] },
    detectedTemplateFamily: 'unknown',
    multiQafVersion: null,
    underlyingQafVersion: null,
    language: 'unknown',
    currencies: [],
    sharedMetadata: {},
    variantDimensions: [],
    activeVariants: [],
    inactiveVariants: [],
    auxiliaryScenarios: [],
    helperColumns: [],
    sharedMaterialMaster: [],
    sharedManufacturingProfiles: [],
    sharedToolingData: [],
    setupCostProfiles: [],
    variantProfileBindings: [],
    summaryAggregation: { perVariant: [], formulaLineageNotes: [] },
    templateFingerprint: { family: 'unknown', structuralHash: 'test-hash', variantColumnCount: 0, headerRowCount: 0, knownProfile: null, classification: null },
    warnings: [],
    confidence: 0.5,
    summaryMoneyRowsByVariant: {},
    ...overrides,
  }
}

function materialRow(overrides: Partial<VariantMatrixRow> = {}): VariantMatrixRow {
  return {
    canonicalComponentIdentity: overrides.canonicalComponentIdentity ?? '10::bracket',
    sourceRow: overrides.sourceRow ?? 10,
    unitCost: overrides.unitCost ?? { value: 1, currency: 'EUR' },
    procurementCurrency: overrides.procurementCurrency ?? 'EUR',
    offerCurrency: overrides.offerCurrency ?? 'EUR',
    exchangeRate: overrides.exchangeRate ?? null,
    logisticsOrDuty: overrides.logisticsOrDuty ?? null,
    materialOverhead: overrides.materialOverhead ?? null,
    quantityFactorByVariant: overrides.quantityFactorByVariant ?? {},
    effectiveCostByVariant: overrides.effectiveCostByVariant ?? {},
    formulaAndCachedValue: overrides.formulaAndCachedValue ?? null,
    sourceCells: overrides.sourceCells ?? [],
    validationStatus: overrides.validationStatus ?? 'ok',
  }
}

function profile(overrides: Partial<SharedCostProfile> = {}): SharedCostProfile {
  return {
    profileId: overrides.profileId ?? 'Fertigungskosten!U10',
    kind: overrides.kind ?? 'manufacturing',
    label: overrides.label ?? null,
    sheet: overrides.sheet ?? 'Fertigungskosten',
    values: overrides.values ?? {},
    formulaAndCachedValue: overrides.formulaAndCachedValue ?? null,
    sourceReferences: overrides.sourceReferences ?? [],
    confidence: overrides.confidence ?? 0.8,
  }
}

function binding(variantId: string, profileId: string): VariantProfileBinding {
  return { variantId, profileId, bindingEvidence: { kind: 'cellReference', source: null, detail: 'test fixture' } }
}

function warning(overrides: Partial<MultiQafWarning> = {}): MultiQafWarning {
  return {
    code: overrides.code ?? 'test_code',
    severity: overrides.severity ?? 'info',
    message: overrides.message ?? 'test warning',
    sourceReferences: overrides.sourceReferences ?? [],
    ...(overrides.variantIds !== undefined ? { variantIds: overrides.variantIds } : {}),
    ...(overrides.reviewRelevant !== undefined ? { reviewRelevant: overrides.reviewRelevant } : {}),
  }
}

function matched(leftId: string, rightId: string): VariantMatchResult {
  return {
    kind: 'matched',
    leftIndex: 0,
    leftId,
    rightIndex: 0,
    rightId,
    stage: 'raw_exact',
    confidence: 1,
    evidence: [],
    explanation: 'test fixture',
    matchedDimensionCount: 1,
  }
}

function unmatchedLeft(leftIndex: number, leftId: string): VariantMatchResult {
  return { kind: 'unmatched_left', leftIndex, leftId, reason: 'removed' }
}

function unmatchedRight(rightIndex: number, rightId: string): VariantMatchResult {
  return { kind: 'unmatched_right', rightIndex, rightId, reason: 'added' }
}

function ambiguous(leftIds: string[], rightIds: string[]): VariantMatchResult {
  return {
    kind: 'ambiguous',
    leftIndices: leftIds.map((_, i) => i),
    leftIds,
    rightIndices: rightIds.map((_, i) => i),
    rightIds,
    candidates: [],
    reviewRelevant: true,
    explanation: 'test fixture ambiguous',
  }
}

/** REAL `variant_profile_binding_unresolved` warning (KAR-944 adversarial
 * review F2) — built by actually RUNNING profile-parser.ts's own
 * `resolveVariantProfileBindings` against a Summary literal that matches NO
 * candidate profile, not by hand-assembling a `MultiQafWarning` object. This
 * is the exact real-producer shape (`unresolvedBindingWarning`,
 * profile-parser.ts): `severity: 'warning'`, NO `reviewRelevant` field set at
 * all — the old aggregate-impact.ts pre-filter (`severity !== 'critical' &&
 * reviewRelevant !== true -> skip`) silently dropped this warning on every
 * real path, masked in the old unit test only because that test manually set
 * `reviewRelevant: true` on a hand-built fixture no real producer ever
 * constructs that way. */
function realUnresolvedBindingWarning(variantId: string): MultiQafWarning {
  const manufacturingRow = 16
  const column = 16 // 0-based grid column -> 'Q' (columnIndexToLetter(17)).
  const summary: ProfileParserGridInput = { sheet: 'Zusammenfassung', grid: gridFromCells({ Q16: 999 }) }
  const { bindings, warnings } = resolveVariantProfileBindings(summary, manufacturingRow, [{ variantId, column }], [])
  if (bindings.length !== 0 || warnings.length !== 1) {
    throw new Error('realUnresolvedBindingWarning fixture assumption broke — resolveVariantProfileBindings no longer returns exactly one unresolved warning for this input.')
  }
  return warnings[0]!
}

/** Assembles the full set of Bausteine computeAggregateImpact needs, running
 * the REAL P2/P3 differ/reconciliation functions over `alt`/`neu` — same
 * pipeline compare-flow.ts's own runMultiQafCompareFlow runs. */
function buildInputs(alt: MultiQafContainer, neu: MultiQafContainer, matchResult: readonly VariantMatchResult[]) {
  const containerDiff = diffContainers(alt, neu, matchResult)
  const materialDiff = diffMaterial(alt, neu, matchResult)
  const profileDiff = diffProfiles(alt, neu, matchResult)
  const reconciliation = { alt: reconcileContainer(alt), neu: reconcileContainer(neu) }
  const reviewRequiredReasons = [...containerDiff.reviewRequiredReasons]
  return { matchResult, materialDiff, profileDiff, containerDiff, reconciliation, reviewRequired: containerDiff.reviewRequired, reviewRequiredReasons }
}

// ── Sign convention + basic per-variant/aggregate happy path ───────────────

describe('computeAggregateImpact — sign convention + happy-path aggregation', () => {
  it('a unit-cost INCREASE (ALT→NEU) produces a POSITIVE impact, scaled by annual/lifetime volume', () => {
    const row = { canonicalComponentIdentity: '10::bracket', quantityFactorByVariant: { V1: 2 }, effectiveCostByVariant: { V1: 2 } }
    const alt = emptyContainer({
      activeVariants: [variant('V1')],
      sharedMaterialMaster: [materialRow({ ...row, unitCost: { value: 1, currency: 'EUR' } })],
    })
    const neu = emptyContainer({
      activeVariants: [variant('W1')],
      sharedMaterialMaster: [materialRow({ ...row, quantityFactorByVariant: { W1: 2 }, effectiveCostByVariant: { W1: 2.2 }, unitCost: { value: 1.1, currency: 'EUR' } })],
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gatesFailed).toEqual([])
    expect(result.unitPriceDeltas).toHaveLength(1)
    expect(result.unitPriceDeltas[0]!.variantId).toBe('W1')
    expect(result.unitPriceDeltas[0]!.byCurrency).toEqual([{ currency: 'EUR', delta: expect.closeTo(0.2, 4) }])

    expect(result.annual.population).toEqual(['W1'])
    expect(result.annual.perVariant[0]!.byCurrency).toEqual([{ currency: 'EUR', delta: expect.closeTo(200, 2) }])
    expect(result.annual.aggregate).toEqual([{ currency: 'EUR', totalImpact: expect.closeTo(200, 2), variantCount: 1 }])

    expect(result.lifetime.population).toEqual(['W1'])
    expect(result.lifetime.aggregate).toEqual([{ currency: 'EUR', totalImpact: expect.closeTo(1000, 2), variantCount: 1 }])
  })

  it('a unit-cost DECREASE (ALT→NEU) produces a NEGATIVE impact (Ersparnis)', () => {
    const row = { canonicalComponentIdentity: '10::bracket', quantityFactorByVariant: { V1: 1 }, effectiveCostByVariant: { V1: 1 } }
    const alt = emptyContainer({
      activeVariants: [variant('V1')],
      sharedMaterialMaster: [materialRow({ ...row, unitCost: { value: 2, currency: 'EUR' } })],
    })
    const neu = emptyContainer({
      activeVariants: [variant('W1')],
      sharedMaterialMaster: [materialRow({ ...row, quantityFactorByVariant: { W1: 1 }, effectiveCostByVariant: { W1: 1.5 }, unitCost: { value: 1.5, currency: 'EUR' } })],
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.unitPriceDeltas[0]!.byCurrency).toEqual([{ currency: 'EUR', delta: expect.closeTo(-0.5, 4) }])
    expect(result.annual.aggregate).toEqual([{ currency: 'EUR', totalImpact: expect.closeTo(-500, 2), variantCount: 1 }])
  })

  it('two currencies for the same variant produce TWO separate buckets, never summed (Buckets-Doktrin)', () => {
    const rowEur = materialRow({
      canonicalComponentIdentity: '10::bracket',
      unitCost: { value: 1, currency: 'EUR' },
      quantityFactorByVariant: { V1: 1 },
      effectiveCostByVariant: { V1: 1 },
    })
    const rowUsd = materialRow({
      canonicalComponentIdentity: '20::screw',
      unitCost: { value: 1, currency: 'USD' },
      quantityFactorByVariant: { V1: 1 },
      effectiveCostByVariant: { V1: 1 },
    })
    const alt = emptyContainer({ activeVariants: [variant('V1')], sharedMaterialMaster: [rowEur, rowUsd] })
    const neu = emptyContainer({
      activeVariants: [variant('W1')],
      sharedMaterialMaster: [
        materialRow({ ...rowEur, quantityFactorByVariant: { W1: 1 }, effectiveCostByVariant: { W1: 1.5 }, unitCost: { value: 1.5, currency: 'EUR' } }),
        materialRow({ ...rowUsd, quantityFactorByVariant: { W1: 1 }, effectiveCostByVariant: { W1: 2 }, unitCost: { value: 2, currency: 'USD' } }),
      ],
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    const buckets = [...result.unitPriceDeltas[0]!.byCurrency].sort((a, b) => a.currency.localeCompare(b.currency))
    expect(buckets).toEqual([
      { currency: 'EUR', delta: expect.closeTo(0.5, 4) },
      { currency: 'USD', delta: expect.closeTo(1, 4) },
    ])
  })
})

// ── Gate: no_duplicates_or_ambiguous ────────────────────────────────────────

describe('computeAggregateImpact — gate: no_duplicates_or_ambiguous', () => {
  it('passes with an empty affected list when there are no ambiguous/split/merge results', () => {
    const alt = emptyContainer({ activeVariants: [variant('V1')] })
    const neu = emptyContainer({ activeVariants: [variant('W1')] })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)
    const gate = result.gates.find((g) => g.gate === 'no_duplicates_or_ambiguous')!
    expect(gate.passed).toBe(true)
    expect(gate.affectedVariantIds).toEqual([])
  })

  it('fails and excludes the affected variants from the population when an ambiguous match exists', () => {
    const alt = emptyContainer({ activeVariants: [variant('V1'), variant('V2')] })
    const neu = emptyContainer({ activeVariants: [variant('W1'), variant('W2')] })
    const inputs = buildInputs(alt, neu, [ambiguous(['V1', 'V2'], ['W1', 'W2'])])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gatesFailed).toContain('no_duplicates_or_ambiguous')
    const gate = result.gates.find((g) => g.gate === 'no_duplicates_or_ambiguous')!
    expect(gate.affectedVariantIds).toEqual(['V1', 'V2', 'W1', 'W2'])
    expect(result.unitPriceDeltas).toEqual([])
    expect(result.excludedVariants.some((e) => e.reason === 'ambiguous_or_uncertain_match')).toBe(true)
  })
})

// ── Gate: no_blocked_critical_mappings ──────────────────────────────────────

describe('computeAggregateImpact — gate: no_blocked_critical_mappings', () => {
  it('fails and excludes a variant referenced by a REAL variant_profile_binding_unresolved warning (KAR-944 adversarial review F2 regression — real producer, no manually-set reviewRelevant)', () => {
    const realWarning = realUnresolvedBindingWarning('W1')
    // Sanity on the fixture itself: this IS the shape the old pre-filter
    // silently dropped (neither condition true) — if either flips, the
    // regression this test guards against has changed shape upstream.
    expect(realWarning.severity).not.toBe('critical')
    expect(realWarning.reviewRelevant).toBeUndefined()
    expect(realWarning.code).toBe('variant_profile_binding_unresolved')

    const alt = emptyContainer({ activeVariants: [variant('V1')] })
    const neu = emptyContainer({ activeVariants: [variant('W1')], warnings: [realWarning] })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gatesFailed).toContain('no_blocked_critical_mappings')
    const gate = result.gates.find((g) => g.gate === 'no_blocked_critical_mappings')!
    expect(gate.affectedVariantIds).toEqual(['W1'])
    expect(result.unitPriceDeltas).toEqual([])
    expect(result.excludedVariants.some((e) => e.variantId === 'W1' && e.reason === 'blocked_critical_mapping')).toBe(true)
  })

  it('passes when no such warning exists', () => {
    const alt = emptyContainer({ activeVariants: [variant('V1')] })
    const neu = emptyContainer({ activeVariants: [variant('W1')] })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)
    expect(result.gates.find((g) => g.gate === 'no_blocked_critical_mappings')!.passed).toBe(true)
  })

  it('blocks via the generic reviewRelevant===true criterion even for a code NOT in BLOCKED_CRITICAL_MAPPING_CODES (KAR-944 F2 — future-producer-proofing, MultiQafWarning.reviewRelevant\'s own documented contract in types.ts)', () => {
    const alt = emptyContainer({ activeVariants: [variant('V1')] })
    const neu = emptyContainer({
      activeVariants: [variant('W1')],
      // A code the module's own named list has never heard of — but
      // reviewRelevant:true is the generic, code-independent signal.
      warnings: [warning({ code: 'some_future_producer_code_not_yet_named', severity: 'warning', reviewRelevant: true, variantIds: ['W1'] })],
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gatesFailed).toContain('no_blocked_critical_mappings')
    expect(result.excludedVariants.some((e) => e.variantId === 'W1' && e.reason === 'blocked_critical_mapping')).toBe(true)
  })

  it('does NOT block on a code in BLOCKED_CRITICAL_MAPPING_CODES when the warning carries no variantIds at all (documents a residual, pre-existing structural gap outside KAR-944 F1-F3 scope: material-matrix-parser.ts\'s real material_matrix_row_identity_collision producer never attaches variantIds — it is a material-ROW identity collision, not a variant-identity one — so this gate structurally cannot exclude a specific variant for that code today)', () => {
    const alt = emptyContainer({ activeVariants: [variant('V1')] })
    const neu = emptyContainer({
      activeVariants: [variant('W1')],
      // Exact real-producer shape (material-matrix-parser.ts, no
      // reviewRelevant, no variantIds — see that module's own
      // 'material_matrix_row_identity_collision' warnings.push call).
      warnings: [
        {
          code: 'material_matrix_row_identity_collision',
          severity: 'warning',
          message: 'test fixture mirroring the real producer shape',
          sourceReferences: [],
        },
      ],
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gates.find((g) => g.gate === 'no_blocked_critical_mappings')!.passed).toBe(true)
    expect(result.unitPriceDeltas.some((d) => d.variantId === 'W1')).toBe(true)
  })
})

// ── Gate: consistent_units (reuses variant-reconciliation.ts's own
// `currency` check) ─────────────────────────────────────────────────────────

describe('computeAggregateImpact — gate: consistent_units', () => {
  it('fails and excludes a variant whose own currency mismatches its material-row currency', () => {
    const alt = emptyContainer({ activeVariants: [variant('V1')] })
    const row = materialRow({
      unitCost: { value: 1, currency: 'USD' },
      quantityFactorByVariant: { W1: 1 },
      effectiveCostByVariant: { W1: 1 },
    })
    const neu = emptyContainer({ activeVariants: [variant('W1', { currency: 'EUR' })], sharedMaterialMaster: [row] })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gatesFailed).toContain('consistent_units')
    const gate = result.gates.find((g) => g.gate === 'consistent_units')!
    expect(gate.affectedVariantIds).toEqual(['W1'])
    expect(result.unitPriceDeltas).toEqual([])
  })

  it('passes when the variant currency and material-row currency agree', () => {
    const alt = emptyContainer({ activeVariants: [variant('V1')] })
    const row = materialRow({ unitCost: { value: 1, currency: 'EUR' }, quantityFactorByVariant: { W1: 1 }, effectiveCostByVariant: { W1: 1 } })
    const neu = emptyContainer({ activeVariants: [variant('W1', { currency: 'EUR' })], sharedMaterialMaster: [row] })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)
    expect(result.gates.find((g) => g.gate === 'consistent_units')!.passed).toBe(true)
  })
})

// ── Gate: consistent_units — 'nicht_pruefbar' (KAR-944 adversarial review F1
// fix). The pre-fix gate tested ONLY `status === 'abweichung'` — a
// `'nicht_pruefbar'` currency check silently PASSED, so an UNVERIFIED
// variant's currency label got trusted as a bucket key. Real manifestation:
// `nichtPruefbarReason: 'fingerprint_unavailable'` when a container's own
// `templateFingerprint.structuralHash` is `null` — variant-reconciliation.ts's
// fail-closed identity guard (`matchToContainer`) then refuses EVERY check
// for that container's variants, currency included (KAR-940 adversarial
// review F2's own fix, reused here one layer up). ──────────────────────────

describe('computeAggregateImpact — gate: consistent_units — nicht_pruefbar (KAR-944 F1)', () => {
  it('excludes (never silently passes) a variant whose currency check is nicht_pruefbar/fingerprint_unavailable', () => {
    const row = materialRow({
      canonicalComponentIdentity: '10::bracket',
      unitCost: { value: 1, currency: 'EUR' },
      quantityFactorByVariant: { V1: 1, W1: 1 },
      effectiveCostByVariant: { V1: 1, W1: 1 },
    })
    const alt = emptyContainer({ activeVariants: [variant('V1', { currency: 'EUR' })], sharedMaterialMaster: [row] })
    const neu = emptyContainer({
      activeVariants: [variant('W1', { currency: 'EUR' })],
      sharedMaterialMaster: [row],
      // The fail-closed identity guard (variant-reconciliation.ts's
      // matchToContainer) rejects EVERY check for this container's own
      // variants when its own structuralHash is null — this is the "real:
      // fingerprint_unavailable" manifestation named in the KAR-944 review.
      templateFingerprint: { family: 'unknown', structuralHash: null, variantColumnCount: 0, headerRowCount: 0, knownProfile: null, classification: null },
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gatesFailed).toContain('consistent_units')
    const gate = result.gates.find((g) => g.gate === 'consistent_units')!
    expect(gate.affectedVariantIds).toEqual(['W1'])
    // W1 is fully excluded from the population — its unverified currency
    // label is never trusted as a bucket key for item 2/4 contributions.
    expect(result.unitPriceDeltas).toEqual([])
    const exclusion = result.excludedVariants.find((e) => e.variantId === 'W1' && e.reason === 'consistent_units_not_verifiable')
    expect(exclusion).toBeDefined()
    expect(exclusion!.messageDe).toContain('fingerprint_unavailable')
    expect(exclusion!.messageEn).toContain('fingerprint_unavailable')
  })

  it('does NOT exclude a variant for the two ROUTINE nicht_pruefbar reasons (mixed_currency / fehlende_daten) — only the identity-guard reasons are fail-closed', () => {
    // fehlende_daten: alt's V1 has zero material rows at all (routine for a
    // variant whose entire cost is profile-driven) — NOT an identity/trust
    // problem. mixed_currency: neu's W1 has EUR+USD rows — the Buckets-
    // Doktrin's own normal case, already handled by per-currency bucketing.
    const alt = emptyContainer({ activeVariants: [variant('V1', { currency: 'EUR' })] })
    const rowEur = materialRow({ canonicalComponentIdentity: '10::a', unitCost: { value: 1, currency: 'EUR' }, quantityFactorByVariant: { W1: 1 }, effectiveCostByVariant: { W1: 1 } })
    const rowUsd = materialRow({ canonicalComponentIdentity: '20::b', unitCost: { value: 1, currency: 'USD' }, quantityFactorByVariant: { W1: 1 }, effectiveCostByVariant: { W1: 1 } })
    const neu = emptyContainer({ activeVariants: [variant('W1', { currency: 'EUR' })], sharedMaterialMaster: [rowEur, rowUsd] })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gates.find((g) => g.gate === 'consistent_units')!.passed).toBe(true)
    expect(result.excludedVariants.some((e) => e.reason === 'consistent_units_not_verifiable')).toBe(false)
    // W1 stays in the population (Buckets-Doktrin — mixed_currency at the
    // ROW level never removes the variant, see gate: compatible_currencies).
    expect(result.unitPriceDeltas.some((d) => d.variantId === 'W1')).toBe(true)
  })
})

// ── Gate: compatible_currencies ─────────────────────────────────────────────

describe('computeAggregateImpact — gate: compatible_currencies', () => {
  it('fails when a material row currency SWAP prevents a numeric impact (material-differ mixed_currency reason)', () => {
    // Each side's own variant currency matches its own row currency (so
    // consistent_units passes) — the swap is ALT-vs-NEU on the row itself.
    const row = { canonicalComponentIdentity: '10::bracket', quantityFactorByVariant: { V1: 1 }, effectiveCostByVariant: { V1: 1 } }
    const alt = emptyContainer({
      activeVariants: [variant('V1', { currency: 'EUR' })],
      sharedMaterialMaster: [materialRow({ ...row, unitCost: { value: 1, currency: 'EUR' } })],
    })
    const neu = emptyContainer({
      activeVariants: [variant('W1', { currency: 'USD' })],
      sharedMaterialMaster: [materialRow({ ...row, quantityFactorByVariant: { W1: 1 }, effectiveCostByVariant: { W1: 1.5 }, unitCost: { value: 1.5, currency: 'USD' } })],
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gatesFailed).toContain('compatible_currencies')
    const gate = result.gates.find((g) => g.gate === 'compatible_currencies')!
    expect(gate.affectedVariantIds).toEqual(['W1'])
    // The variant STAYS in the population (buckets doctrine — no fabricated
    // cross-currency number, but no false removal either); it simply has no
    // computable contribution from this one component.
    expect(result.unitPriceDeltas).toHaveLength(1)
    expect(result.unitPriceDeltas[0]!.byCurrency).toEqual([])
  })
})

// ── Gate: volumes_available (per-timeframe exclusion) ───────────────────────

describe('computeAggregateImpact — gate: volumes_available', () => {
  it('excludes a variant from the ANNUAL population only when annualVolume is missing (lifetime unaffected)', () => {
    const alt = emptyContainer({ activeVariants: [variant('V1')] })
    const neu = emptyContainer({ activeVariants: [variant('W1', { annualVolume: null, lifetimeVolume: 5000 })] })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gatesFailed).toContain('volumes_available')
    expect(result.annual.population).toEqual([])
    expect(result.lifetime.population).toEqual(['W1'])
    expect(result.excludedVariants.some((e) => e.variantId === 'W1' && e.reason === 'volume_missing_annual')).toBe(true)
    expect(result.excludedVariants.some((e) => e.reason === 'volume_missing_lifetime')).toBe(false)
  })

  it('passes when every population variant carries both volume metrics', () => {
    const alt = emptyContainer({ activeVariants: [variant('V1')] })
    const neu = emptyContainer({ activeVariants: [variant('W1', { annualVolume: 100, lifetimeVolume: 500 })] })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)
    expect(result.gates.find((g) => g.gate === 'volumes_available')!.passed).toBe(true)
  })
})

// ── Gate: valid_baseline (suppression + acknowledgment) ─────────────────────

describe('computeAggregateImpact — gate: valid_baseline', () => {
  function buildReviewRequiredScenario() {
    const row = { canonicalComponentIdentity: '10::bracket', quantityFactorByVariant: { V1: 1 }, effectiveCostByVariant: { V1: 1 } }
    const alt = emptyContainer({
      activeVariants: [variant('V1')],
      sharedMaterialMaster: [materialRow({ ...row, unitCost: { value: 1, currency: 'EUR' } })],
      warnings: [warning({ code: 'some_other_finding', severity: 'critical' })],
    })
    const neu = emptyContainer({
      activeVariants: [variant('W1')],
      sharedMaterialMaster: [materialRow({ ...row, quantityFactorByVariant: { W1: 1 }, effectiveCostByVariant: { W1: 1.5 }, unitCost: { value: 1.5, currency: 'EUR' } })],
    })
    return { alt, neu, inputs: buildInputs(alt, neu, [matched('V1', 'W1')]) }
  }

  it('suppresses the SUMMED aggregate (empty population) when the baseline is invalid and not acknowledged, but keeps per-variant tables + structural fallback', () => {
    const { alt, neu, inputs } = buildReviewRequiredScenario()
    expect(inputs.reviewRequired).toBe(true) // sanity: alt's own critical warning drives this.

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.gatesFailed).toContain('valid_baseline')
    expect(result.reviewRequired).toBe(true)
    expect(result.annual.population).toEqual([])
    expect(result.annual.aggregate).toEqual([])
    expect(result.lifetime.population).toEqual([])
    expect(result.lifetime.aggregate).toEqual([])
    // Per-variant figures and the unweighted structural fallback stay available.
    expect(result.unitPriceDeltas).toHaveLength(1)
    expect(result.annual.perVariant).toHaveLength(1)
    expect(result.structuralFallback).toBeDefined()
    expect(result.assumptions.some((a) => a.code === 'aggregate_suppressed_baseline_invalid')).toBe(true)
  })

  it('computes the SUMMED aggregate when acknowledged, but reviewRequired stays true', () => {
    const { alt, neu, inputs } = buildReviewRequiredScenario()

    const result = computeAggregateImpact(alt, neu, inputs, { acknowledgeReviewRequired: true })

    expect(result.gates.find((g) => g.gate === 'valid_baseline')!.passed).toBe(true)
    expect(result.reviewRequired).toBe(true)
    expect(result.annual.population).toEqual(['W1'])
    expect(result.annual.aggregate).not.toEqual([])
    expect(result.assumptions.some((a) => a.code === 'aggregate_computed_despite_review')).toBe(true)
  })
})

// ── profile-differ item 2 (totalChanges) + item 4 (bindingValueImpacts) ────

describe('computeAggregateImpact — manufacturing deltas (profile-differ item 2/4)', () => {
  it('includes a shared profile total change (item 2) when the variant currency ALT=NEU is known', () => {
    const p = profile({ profileId: 'Fertigungskosten!U10', kind: 'manufacturing', values: { totalPerUnit: 10 } })
    const alt = emptyContainer({
      activeVariants: [variant('V1', { currency: 'EUR' })],
      sharedManufacturingProfiles: [p],
      variantProfileBindings: [binding('V1', p.profileId)],
    })
    const neu = emptyContainer({
      activeVariants: [variant('W1', { currency: 'EUR' })],
      sharedManufacturingProfiles: [profile({ ...p, values: { totalPerUnit: 12 } })],
      variantProfileBindings: [binding('W1', p.profileId)],
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.unitPriceDeltas[0]!.byCurrency).toEqual([{ currency: 'EUR', delta: expect.closeTo(2, 4) }])
    expect(result.unitPriceDeltas[0]!.components.some((c) => c.source === 'profile_total_change')).toBe(true)
  })

  it('excludes item 2 when the variant currency ALT vs NEU differs, and flags compatible_currencies', () => {
    const p = profile({ profileId: 'Fertigungskosten!U10', kind: 'manufacturing', values: { totalPerUnit: 10 } })
    const alt = emptyContainer({
      activeVariants: [variant('V1', { currency: 'USD' })],
      sharedManufacturingProfiles: [p],
      variantProfileBindings: [binding('V1', p.profileId)],
    })
    const neu = emptyContainer({
      activeVariants: [variant('W1', { currency: 'EUR' })],
      sharedManufacturingProfiles: [profile({ ...p, values: { totalPerUnit: 12 } })],
      variantProfileBindings: [binding('W1', p.profileId)],
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.unitPriceDeltas[0]!.byCurrency).toEqual([])
    expect(result.gatesFailed).toContain('compatible_currencies')
  })

  it('includes a binding value impact (item 4) when currencyState is same_currency', () => {
    // Distinct LABELS so the two profiles carry distinct structural identity
    // (profile-differ.ts/container-differ.ts key on kind+label+ordinal, not
    // profileId) — otherwise the binding would resolve to the SAME
    // structural profile on both sides and produce no rebind at all.
    const pManu = profile({ profileId: 'Fertigungskosten!U10', kind: 'manufacturing', label: 'Profil A', values: { totalPerUnit: 5 } })
    const pManu2 = profile({ profileId: 'Fertigungskosten!U20', kind: 'manufacturing', label: 'Profil B', values: { totalPerUnit: 8 } })
    const alt = emptyContainer({
      activeVariants: [variant('V1', { currency: 'EUR' })],
      sharedManufacturingProfiles: [pManu],
      variantProfileBindings: [binding('V1', pManu.profileId)],
    })
    const neu = emptyContainer({
      activeVariants: [variant('W1', { currency: 'EUR' })],
      sharedManufacturingProfiles: [pManu2],
      variantProfileBindings: [binding('W1', pManu2.profileId)],
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.unitPriceDeltas[0]!.components.some((c) => c.source === 'profile_binding_value_impact' && c.delta > 0)).toBe(true)
  })
})

// ── Unweighted structural fallback (always available) ───────────────────────

describe('computeAggregateImpact — structural fallback', () => {
  it('is always populated from container-differ counts, even with zero matched variants', () => {
    const alt = emptyContainer({ activeVariants: [variant('V1'), variant('V2')] })
    const neu = emptyContainer({ activeVariants: [variant('W1'), variant('W2'), variant('W3')] })
    const inputs = buildInputs(alt, neu, [
      unmatchedLeft(0, 'V1'),
      unmatchedLeft(1, 'V2'),
      unmatchedRight(0, 'W1'),
      unmatchedRight(1, 'W2'),
      unmatchedRight(2, 'W3'),
    ])

    const result = computeAggregateImpact(alt, neu, inputs)

    expect(result.structuralFallback.variantsRemoved).toBe(2)
    expect(result.structuralFallback.variantsAdded).toBe(3)
    expect(result.unitPriceDeltas).toEqual([])
    expect(result.annual.population).toEqual([])
    expect(result.lifetime.population).toEqual([])
    expect(result.assumptions.some((a) => a.code === 'no_matched_variants')).toBe(true)
  })
})

// ── Determinism ──────────────────────────────────────────────────────────

describe('computeAggregateImpact — determinism', () => {
  it('produces a byte-identical (JSON.stringify-equal) result for identical inputs', () => {
    const row = { canonicalComponentIdentity: '10::bracket', quantityFactorByVariant: { V1: 2 }, effectiveCostByVariant: { V1: 2 } }
    const alt = emptyContainer({
      activeVariants: [variant('V1')],
      sharedMaterialMaster: [materialRow({ ...row, unitCost: { value: 1, currency: 'EUR' } })],
    })
    const neu = emptyContainer({
      activeVariants: [variant('W1')],
      sharedMaterialMaster: [materialRow({ ...row, quantityFactorByVariant: { W1: 2 }, effectiveCostByVariant: { W1: 2.2 }, unitCost: { value: 1.1, currency: 'EUR' } })],
    })
    const inputs = buildInputs(alt, neu, [matched('V1', 'W1')])

    const a = computeAggregateImpact(alt, neu, inputs)
    const b = computeAggregateImpact(alt, neu, inputs)

    expect(JSON.stringify(a)).toBe(JSON.stringify(b))
  })
})
