// A18 Management-Analyse (§18) — Wertstrom P7.
// FIXTURE-DATEN-REGEL: every id/name/number below is FREE INVENTION.

import { describe, expect, it } from 'vitest'
import type { VsmNode } from '@/lib/vsm-types'
import { computeManagementAnalysis, type AnalysisCategory, type ManagementAnalysisResult, type ScenarioEffectRow } from '../internal/management-analysis'
import { KNOWN_VALIDATION_GAPS } from '../internal/validation'

function makeNode(id: string, overrides: Partial<VsmNode> = {}): VsmNode {
  return { id, type: 'process', x: 0, y: 0, name: id, ...overrides }
}

/** Every real engine symbol a `sourceMetric` is allowed to start with —
 * "Traceability" test below fails closed if a statement ever cites
 * something outside this list (a free-text label, not a real function). */
const KNOWN_SOURCE_PREFIXES = [
  'context.demandUnitsPerDay',
  'context.taktTimeSec',
  'context.openMeasures',
  'context.openMeasuresIssue',
  'computeBottleneckV2',
  'computeTimelineLadder',
  'computeInventoryCoverage',
  'computeCumulativeYield',
  'runValidation',
  'nodes.filter(type=inventory)',
  'computeKpiDeltaRows',
  'computeBottleneckV2 / computeInventoryCoverage / computeTimelineLadder',
]

function categoriesOf(result: ManagementAnalysisResult): AnalysisCategory[] {
  return result.statements.map((s) => s.category)
}

describe('computeManagementAnalysis — traceability', () => {
  it('every statement cites a real engine symbol in sourceMetric', () => {
    const nodes = [
      makeNode('welding', { name: 'Schweißen', cycleTimeSec: 90, oee: 85, isValueAdded: true, scrapRate: 2 }),
      makeNode('paint', { name: 'Lackieren', cycleTimeSec: 60, oee: 90, isValueAdded: true, scrapRate: 1 }),
      makeNode('buffer', { id: 'buffer', name: 'Puffer', type: 'inventory', x: 0, y: 0, quantity: 480 }),
      { id: 'cust', type: 'customer', x: 0, y: 0, name: 'Kunde', demand: 240 } satisfies VsmNode,
    ]
    const result = computeManagementAnalysis(nodes, [], { taktTimeSec: 100, demandUnitsPerDay: 240 })
    expect(result.statements.length).toBeGreaterThan(0)
    for (const s of result.statements) {
      expect(s.sourceMetric.length).toBeGreaterThan(0)
      const matches = KNOWN_SOURCE_PREFIXES.some((p) => s.sourceMetric.startsWith(p))
      expect(matches, `unexpected sourceMetric "${s.sourceMetric}" on statement "${s.id}"`).toBe(true)
      // "value(s)" — every statement carries at least the keys its own text
      // references (possibly null, never simply omitted).
      expect(typeof s.values).toBe('object')
    }
  })

  it('passes KNOWN_VALIDATION_GAPS through verbatim (informational, not recomputed)', () => {
    const result = computeManagementAnalysis([], [], {})
    expect(result.knownGaps).toBe(KNOWN_VALIDATION_GAPS)
  })
})

describe('computeManagementAnalysis — every §18 category present (rich snapshot)', () => {
  const nodes: VsmNode[] = [
    { id: 'sup', type: 'supplier', x: 0, y: 0, name: 'Lieferant' },
    makeNode('welding', { name: 'Schweißen', cycleTimeSec: 90, oee: 85, isValueAdded: true, scrapRate: 2 }),
    makeNode('paint', { name: 'Lackieren', cycleTimeSec: 130, oee: 80, isValueAdded: true, scrapRate: 1 }),
    { id: 'buffer', type: 'inventory', x: 0, y: 0, name: 'Puffer vor Lackieren', quantity: 480 },
    { id: 'cust', type: 'customer', x: 0, y: 0, name: 'Kunde', demand: 240 },
  ]
  const context = { taktTimeSec: 100, demandUnitsPerDay: 240, shiftModel: { hoursPerShift: 8, shiftsPerDay: 2, breakMinPerShift: 30 } }

  it('produces exactly one statement for every singular §18 category', () => {
    const result = computeManagementAnalysis(nodes, [], context)
    const cats = categoriesOf(result)
    for (const cat of ['demand', 'takt', 'lead-time', 'value-add-time', 'value-add-share', 'quality', 'capacity-risk'] as const) {
      expect(cats.filter((c) => c === cat).length, `category ${cat}`).toBe(1)
    }
    // List-shaped categories: at least one.
    for (const cat of ['bottleneck-primary', 'bottleneck-secondary', 'inventory-hotspot', 'improvement'] as const) {
      expect(cats.filter((c) => c === cat).length, `category ${cat}`).toBeGreaterThanOrEqual(1)
    }
  })

  it('demand statement: real value, high confidence, traceable', () => {
    const result = computeManagementAnalysis(nodes, [], context)
    const s = result.statements.find((s) => s.category === 'demand')!
    expect(s.text).toBe('Der Kundenbedarf beträgt 240 Stück pro Tag.')
    expect(s.confidence).toBe('high')
    expect(s.values.demandUnitsPerDay).toBe(240)
  })

  it('takt statement: real value', () => {
    const result = computeManagementAnalysis(nodes, [], context)
    const s = result.statements.find((s) => s.category === 'takt')!
    expect(s.text).toBe('Der Kundentakt beträgt 100 Sekunden.')
    expect(s.confidence).toBe('high')
  })

  it('lead-time: "medium" when only some process-time categories are fully measured (partial coverage)', () => {
    const result = computeManagementAnalysis(nodes, [], context)
    const s = result.statements.find((s) => s.category === 'lead-time')!
    // Fixture nodes (welding/paint) carry cycleTimeSec but never waitTimeSec
    // -> waitingTimeSec category is unmeasured while vaTimeSec is fully
    // measured and inventory is computable -> partial coverage.
    expect(s.confidence).toBe('medium')
  })

  it('primary bottleneck: reuses computeBottleneckV2.reasonDe verbatim', () => {
    // effCT welding = 90/0.85*... -- delegated to capacity.ts, already
    // covered by bottleneck.test.ts; this test only checks the ANALYSIS
    // layer reuses the engine's own sentence and confidence unmodified.
    const result = computeManagementAnalysis(nodes, [], context)
    const s = result.statements.find((s) => s.category === 'bottleneck-primary')!
    expect(s.text).toContain('ist der primäre Engpass')
    expect(s.sourceMetric).toBe('computeBottleneckV2.primary.reasonDe')
    expect(['high', 'medium', 'low']).toContain(s.confidence)
    expect(s.values.nodeName).toBeTruthy()
  })

  it('inventory hotspot: names the buffer node and its coverage in days', () => {
    // coverageDays = 480 / 240 = 2 days.
    const result = computeManagementAnalysis(nodes, [], context)
    const s = result.statements.find((s) => s.category === 'inventory-hotspot')!
    expect(s.text).toContain('Puffer vor Lackieren')
    expect(s.text).toContain('2 Tagen Reichweite')
    expect(s.values.topCoverageDays).toBe(2)
    expect(s.confidence).toBe('high')
  })

  it('quality statement: cumulative yield from scrap-only inputs, rework caveat included', () => {
    // yield = (1-0.02)*(1-0.01)*100 = 97.02%.
    const result = computeManagementAnalysis(nodes, [], context)
    const s = result.statements.find((s) => s.category === 'quality')!
    expect(s.values.cumulativeYieldPct).toBeCloseTo(97.02, 2)
    expect(s.text).toContain('Nacharbeit ist in dieser Zahl nicht enthalten')
  })

  it('open measures: absent context -> not-computable, explicit gap sentence', () => {
    const result = computeManagementAnalysis(nodes, [], context)
    const s = result.statements.find((s) => s.category === 'open-measures')!
    expect(s.confidence).toBe('not-computable')
    expect(s.text).toContain('nicht bewertbar')
  })

  it('open measures: empty array is a real "none", not a gap', () => {
    const result = computeManagementAnalysis(nodes, [], { ...context, openMeasures: [] })
    const s = result.statements.find((s) => s.category === 'open-measures')!
    expect(s.confidence).toBe('high')
    expect(s.text).toBe('Keine offenen Maßnahmen hinterlegt.')
  })

  it('open measures: real data source produces a traceable list', () => {
    const result = computeManagementAnalysis(nodes, [], { ...context, openMeasures: [{ id: 'm1', title: 'Rüstzeit reduzieren' }] })
    const s = result.statements.find((s) => s.category === 'open-measures')!
    expect(s.confidence).toBe('high')
    expect(s.text).toBe('1 offene Maßnahme(n) hinterlegt: Rüstzeit reduzieren.')
  })

  // Wertstrom P8.1 fix (K2, RLS-Sichtbarkeits-Ehrlichkeit — PR #363
  // adversarial review): `openMeasuresIssue` picks a HONEST, more specific
  // "nicht bewertbar"-Satz than the generic "keine Datenquelle" one — never
  // "0 offene Maßnahmen" for a result the caller could not actually confirm.
  it('open measures: openMeasuresIssue "not-visible" -> honest "nicht einsehbar", never "0 offene Maßnahmen"', () => {
    const result = computeManagementAnalysis(nodes, [], { ...context, openMeasuresIssue: 'not-visible' })
    const s = result.statements.find((s) => s.category === 'open-measures')!
    expect(s.confidence).toBe('not-computable')
    expect(s.text).toBe('Offene Maßnahmen sind nicht bewertbar, weil sie für diese Ansicht nicht einsehbar sind (kein Zugriff auf das verknüpfte Projekt).')
    expect(s.text).not.toContain('Keine offenen Maßnahmen hinterlegt')
  })

  it('open measures: openMeasuresIssue "load-error" -> honest "konnte nicht geladen werden"', () => {
    const result = computeManagementAnalysis(nodes, [], { ...context, openMeasuresIssue: 'load-error' })
    const s = result.statements.find((s) => s.category === 'open-measures')!
    expect(s.confidence).toBe('not-computable')
    expect(s.text).toBe('Offene Maßnahmen konnten nicht geladen werden.')
  })

  it('open measures: a real openMeasures array wins even if openMeasuresIssue were (incorrectly) also set', () => {
    const result = computeManagementAnalysis(nodes, [], { ...context, openMeasures: [], openMeasuresIssue: 'not-visible' })
    const s = result.statements.find((s) => s.category === 'open-measures')!
    expect(s.text).toBe('Keine offenen Maßnahmen hinterlegt.')
  })

  it('scenario-effect: absent entirely without an active comparison (not a "nicht bewertbar" placeholder)', () => {
    const result = computeManagementAnalysis(nodes, [], context)
    expect(categoriesOf(result)).not.toContain('scenario-effect')
  })

  it('scenario-effect: present + traceable once a comparison is supplied', () => {
    const rows: ScenarioEffectRow[] = [
      { key: 'leadTime', label: 'Durchlaufzeit (DLZ)', unit: 'seconds', currentValue: 1000, scenarioValue: 720, absoluteDelta: -280, percentDelta: -28, direction: 'improvement' },
      { key: 'vaTime', label: 'Wertschöpfende Zeit (VA)', unit: 'seconds', currentValue: 220, scenarioValue: null, absoluteDelta: null, percentDelta: null, direction: 'not-comparable' },
    ]
    const result = computeManagementAnalysis(nodes, [], { ...context, scenarioComparison: { scenarioLabel: 'Soll 2027', rows } })
    const effects = result.statements.filter((s) => s.category === 'scenario-effect')
    expect(effects).toHaveLength(2)
    // C3-Fix: the "improvement" verb alone carries the direction — the
    // amount reads as a positive magnitude (Math.abs), never "verbessert
    // sich ... um -280" (a literal improvement by a negative amount).
    expect(effects[0].text).toContain('Durchlaufzeit (DLZ) verbessert sich im Szenario „Soll 2027" um 280 Sekunden (-28%).')
    expect(effects[0].confidence).toBe('high')
    expect(effects[1].confidence).toBe('not-computable')
    expect(effects[1].text).toContain('nicht vergleichbar')
  })

  it('scenario-effect: an "info"-direction row (no better/worse verb) keeps a signed amount, since only the sign itself signals growth/shrinkage', () => {
    const rows: ScenarioEffectRow[] = [
      { key: 'vaTime', label: 'Wertschöpfende Zeit (VA)', unit: 'seconds', currentValue: 200, scenarioValue: 250, absoluteDelta: 50, percentDelta: 25, direction: 'info' },
      { key: 'vaTime', label: 'Wertschöpfende Zeit (VA)', unit: 'seconds', currentValue: 200, scenarioValue: 150, absoluteDelta: -50, percentDelta: -25, direction: 'info' },
    ]
    const result = computeManagementAnalysis(nodes, [], { ...context, scenarioComparison: { scenarioLabel: 'Soll 2027', rows } })
    const effects = result.statements.filter((s) => s.category === 'scenario-effect')
    expect(effects[0].text).toContain('Wertschöpfende Zeit (VA) verändert sich im Szenario „Soll 2027" um +50 Sekunden')
    expect(effects[1].text).toContain('Wertschöpfende Zeit (VA) verändert sich im Szenario „Soll 2027" um -50 Sekunden')
  })

  it('scenario-effect: a "deterioration" row also reads a positive magnitude (e.g. PCE dropping)', () => {
    const rows: ScenarioEffectRow[] = [
      { key: 'pce', label: 'Process Cycle Efficiency (PCE)', unit: 'percent', currentValue: 40, scenarioValue: 30, absoluteDelta: -10, percentDelta: -25, direction: 'deterioration' },
    ]
    const result = computeManagementAnalysis(nodes, [], { ...context, scenarioComparison: { scenarioLabel: 'Soll 2027', rows } })
    const effects = result.statements.filter((s) => s.category === 'scenario-effect')
    expect(effects[0].text).toContain('Process Cycle Efficiency (PCE) verschlechtert sich im Szenario „Soll 2027" um 10 Prozentpunkte (-25%).')
  })

  it('scenario-effect: a context mismatch prepends an explicit caveat sentence and downgrades confidence to medium', () => {
    const rows: ScenarioEffectRow[] = [
      { key: 'leadTime', label: 'Durchlaufzeit (DLZ)', unit: 'seconds', currentValue: 1000, scenarioValue: 720, absoluteDelta: -280, percentDelta: -28, direction: 'improvement' },
    ]
    const result = computeManagementAnalysis(nodes, [], {
      ...context,
      scenarioComparison: { scenarioLabel: 'Soll 2027', rows, contextMismatch: { differentTakt: true, differentShiftModel: false, differentDemand: false, any: true } },
    })
    const effects = result.statements.filter((s) => s.category === 'scenario-effect')
    expect(effects).toHaveLength(2)
    expect(effects[0].id).toBe('scenario-effect-context-caveat')
    expect(effects[0].text).toContain('Vergleich unter abweichenden Rahmenbedingungen')
    expect(effects[0].text).toContain('einen unterschiedlichen Kundentakt')
    expect(effects[0].confidence).toBe('high')
    expect(effects[1].confidence).toBe('medium')
  })

  it('scenario-effect: no context mismatch -> no caveat sentence, full "high" confidence (unchanged behavior)', () => {
    const rows: ScenarioEffectRow[] = [
      { key: 'leadTime', label: 'Durchlaufzeit (DLZ)', unit: 'seconds', currentValue: 1000, scenarioValue: 720, absoluteDelta: -280, percentDelta: -28, direction: 'improvement' },
    ]
    const result = computeManagementAnalysis(nodes, [], {
      ...context,
      scenarioComparison: { scenarioLabel: 'Soll 2027', rows, contextMismatch: { differentTakt: false, differentShiftModel: false, differentDemand: false, any: false } },
    })
    const effects = result.statements.filter((s) => s.category === 'scenario-effect')
    expect(effects).toHaveLength(1)
    expect(effects[0].confidence).toBe('high')
  })

  it('German number format: a genuinely fractional value renders with a comma, never a JS-standard dot', () => {
    const result = computeManagementAnalysis(nodes, [], { ...context, taktTimeSec: 46.15 })
    const s = result.statements.find((s) => s.category === 'takt')!
    expect(s.text).toBe('Der Kundentakt beträgt 46,2 Sekunden.')
    expect(s.text).not.toMatch(/\d\.\d/)
  })
})

// P8.2a (KAR-878/KAR-986, Baustein 1 "isVaNode-unknown-Alignment"):
// leadTimeConfidence's processCategories array must include the NEW
// unknownTimeSec bucket — otherwise a vaClass='unknown' node's measured
// cycleTimeSec would count towards NOTHING in the completeness check
// (moved out of nonVaProcessTimeSec into a bucket the check didn't know
// about), silently degrading confidence for any Wertstrom containing one.
describe('computeManagementAnalysis — P8.2a Baustein 1 (unknown-Alignment, lead-time confidence)', () => {
  it('a Wertstrom whose ONLY time-carrying node is vaClass=\'unknown\' is NOT reported "nicht bewertbar" — its measured cycleTimeSec counts as real, confirmed process time', () => {
    const nodes: VsmNode[] = [makeNode('n1', { name: 'Ungeklärter Prozess', vaClass: 'unknown', cycleTimeSec: 50 })]
    const result = computeManagementAnalysis(nodes, [])
    const s = result.statements.find((s) => s.category === 'lead-time')!
    expect(s.confidence).not.toBe('not-computable')
    expect(s.values.totalLeadTimeSec).toBe(50)
  })

  it('fully measured process time (incl. a vaClass=\'unknown\' node) + inventory reaches "high" confidence, matching the pre-existing fully-classified case', () => {
    const nodes: VsmNode[] = [
      makeNode('unknown', { vaClass: 'unknown', cycleTimeSec: 20, waitTimeSec: 0 }),
      makeNode('va', { vaClass: 'va', cycleTimeSec: 30, waitTimeSec: 0 }),
    ]
    const result = computeManagementAnalysis(nodes, [], { demandUnitsPerDay: 100, shiftModel: { hoursPerShift: 8, shiftsPerDay: 1, breakMinPerShift: 0 } })
    const s = result.statements.find((s) => s.category === 'lead-time')!
    expect(s.confidence).toBe('high')
  })
})

describe('computeManagementAnalysis — sparse snapshot (every "nicht bewertbar" branch)', () => {
  it('with zero nodes and no context: demand/takt/bottleneck/quality/inventory/open-measures all "nicht bewertbar"', () => {
    const result = computeManagementAnalysis([], [], undefined)
    const notComputable = result.statements.filter((s) => s.confidence === 'not-computable')
    const notComputableCats = notComputable.map((s) => s.category)
    expect(notComputableCats).toEqual(
      expect.arrayContaining(['demand', 'takt', 'bottleneck-primary', 'bottleneck-secondary', 'lead-time', 'value-add-time', 'value-add-share', 'quality', 'open-measures', 'improvement']),
    )
    for (const s of notComputable) {
      expect(s.text).toContain('nicht bewertbar')
    }
    // Inventory hotspot on an EMPTY graph is a real "no inventory nodes"
    // fact, not a gap — must NOT be reported as not-computable.
    const inventoryStatement = result.statements.find((s) => s.category === 'inventory-hotspot')!
    expect(inventoryStatement.confidence).toBe('high')
    expect(inventoryStatement.text).toBe('Der Wertstrom enthält keine Bestands-/Puffer-Nodes.')
  })

  // C1-Fix: a completely empty snapshot used to report the lead-time
  // statement as 'high' confidence for a "0 Sekunden" figure built entirely
  // from unmeasured-as-zero defaults — exactly the "null ≠ 0" doctrine this
  // module exists to enforce, violated for lead-time alone. Now honestly
  // "nicht bewertbar", consistent with every sibling category.
  it('lead-time: a completely empty snapshot is "nicht bewertbar", never a fabricated "0 Sekunden / high"', () => {
    const result = computeManagementAnalysis([], [], undefined)
    const s = result.statements.find((s) => s.category === 'lead-time')!
    expect(s.confidence).toBe('not-computable')
    expect(s.text).toContain('nicht bewertbar')
    expect(s.values.totalLeadTimeSec).toBeNull()
  })

  it('lead-time: "low" when the ENTIRE total rests solely on a computable inventory estimate — zero process-side measurement at all', () => {
    // Reproduces the exact C1 finding scenario: one process node with no
    // cycleTimeSec/waitTimeSec (contributes nothing measured) + one
    // inventory node with a real quantity, full demand/shiftModel context so
    // Little's Law CAN compute inventory time. The resulting non-zero total
    // is real (not fabricated) but has zero grounding in direct
    // measurement — 'low', not 'high'.
    const sparseNodes: VsmNode[] = [
      makeNode('montage', { name: 'Montage' }),
      { id: 'buf', type: 'inventory', x: 0, y: 0, name: 'Pufferlager', quantity: 1000 },
    ]
    const result = computeManagementAnalysis(sparseNodes, [], {
      demandUnitsPerDay: 250,
      shiftModel: { hoursPerShift: 8, shiftsPerDay: 1, breakMinPerShift: 30 },
    })
    const s = result.statements.find((s) => s.category === 'lead-time')!
    expect(s.confidence).toBe('low')
    expect(s.values.totalLeadTimeSec).toBeGreaterThan(0)
  })

  it('bottleneck-secondary is "nicht bewertbar" (not "0 gefunden") when the primary is already degraded', () => {
    const nodes = [makeNode('only', { cycleTimeSec: 42 })]
    const result = computeManagementAnalysis(nodes, [], { taktTimeSec: 30 })
    const s = result.statements.find((s) => s.category === 'bottleneck-secondary')!
    expect(s.confidence).toBe('not-computable')
  })

  it('bottleneck-secondary reports a real "0 gefunden" (not a gap) when the full model runs but nothing is near takt', () => {
    const nodes = [makeNode('a', { cycleTimeSec: 10, oee: 100 }), makeNode('b', { cycleTimeSec: 12, oee: 100 })]
    const result = computeManagementAnalysis(nodes, [], { taktTimeSec: 1000 })
    const s = result.statements.find((s) => s.category === 'bottleneck-secondary')!
    expect(s.confidence).not.toBe('not-computable')
    expect(s.text).toContain('Keine weiteren Prozesse')
  })

  it('inventory hotspot: nodes exist but no demand -> "nicht bewertbar"', () => {
    const nodes = [{ id: 'buf', type: 'inventory' as const, x: 0, y: 0, name: 'Puffer', quantity: 10 }]
    const result = computeManagementAnalysis(nodes, [], {})
    const s = result.statements.find((s) => s.category === 'inventory-hotspot')!
    expect(s.confidence).toBe('not-computable')
  })

  it('capacity-risk: a real "0 gefunden" result (not a gap) when validation runs but finds nothing', () => {
    const nodes = [makeNode('a', { cycleTimeSec: 10 })]
    const result = computeManagementAnalysis(nodes, [], {})
    const s = result.statements.find((s) => s.category === 'capacity-risk')!
    expect(s.confidence).toBe('high')
    expect(s.text).toContain('keine Kapazitätsrisiken')
  })

  it('capacity-risk: surfaces real validation issues with critical/warning counts', () => {
    const nodes = [makeNode('a', { cycleTimeSec: 10, oee: 150 })] // OEE out of range -> critical
    const result = computeManagementAnalysis(nodes, [], {})
    const s = result.statements.find((s) => s.category === 'capacity-risk')!
    expect(s.confidence).toBe('high')
    expect(s.values.criticalCount).toBe(1)
  })

  // C15-Fix: a real, non-zero PCE below the 1-decimal rounding threshold
  // (dominant inventory time vs. a short cycle time — the Lean-context norm,
  // not a construct) used to render as "0 Prozent", visually identical to a
  // genuine zero, even though `values.pcePct` kept the exact number.
  it('value-add-share: a tiny but real (non-zero) PCE renders "unter 0,1 Prozent", never a bare "0"', () => {
    const nodes = [
      makeNode('kleinteil', { name: 'Kleinteil', cycleTimeSec: 5, isValueAdded: true }),
      { id: 'lager', type: 'inventory' as const, x: 0, y: 0, name: 'Langzeitlager', quantity: 5000 },
    ]
    const result = computeManagementAnalysis(nodes, [], { demandUnitsPerDay: 50, shiftModel: { hoursPerShift: 8, shiftsPerDay: 1, breakMinPerShift: 0 } })
    const s = result.statements.find((s) => s.category === 'value-add-share')!
    expect(s.values.pcePct).toBeGreaterThan(0)
    expect(s.values.pcePct as number).toBeLessThan(0.05)
    expect(s.text).toBe('Nur unter 0,1 Prozent der gesamten Durchlaufzeit sind wertschöpfend.')
    expect(s.text).not.toContain('Nur 0 Prozent')
  })
})
