// A10/B5 Quality Impact — scrap, rework, yield (§8.7) — hand-calculated
// tests, incl. the mandated 0%/100% rework edge cases and the "scrap !=
// rework" doctrine (rework must NOT reduce cumulative yield).
// FIXTURE-DATEN-REGEL: every number below is FREE INVENTION.

import { describe, expect, it } from 'vitest'
import { computeCumulativeYield, computeExpectedGoodOutput, computeProcessQualityImpact, type ProcessQualityInput } from '../internal/quality'

describe('computeProcessQualityImpact', () => {
  it('hand calc: 5% scrap + 10% rework (20s each) -> 85% FPY, 2s additional rework capacity', () => {
    // FPY = 100 - 5 - 10 = 85. Rework capacity = 10/100 * 20s = 2s.
    const result = computeProcessQualityImpact({ nodeId: 'weld', scrapRatePct: 5, reworkRatePct: 10, reworkTimeSec: 20 })
    expect(result.firstPassYieldPct).toBe(85)
    expect(result.additionalReworkCapacityDemandSec).toBe(2)
  })

  it('edge case: 0% rework -> 0 additional capacity demand (a real, computable zero, not null)', () => {
    const result = computeProcessQualityImpact({ nodeId: 'weld', scrapRatePct: 0, reworkRatePct: 0 })
    expect(result.firstPassYieldPct).toBe(100)
    expect(result.additionalReworkCapacityDemandSec).toBe(0)
  })

  it('edge case: 100% rework (every unit reworked, 15s each) -> 0% FPY, 15s additional capacity per unit', () => {
    const result = computeProcessQualityImpact({ nodeId: 'weld', reworkRatePct: 100, reworkTimeSec: 15 })
    expect(result.firstPassYieldPct).toBe(0)
    expect(result.additionalReworkCapacityDemandSec).toBe(15)
  })

  it('neither scrap nor rework known -> firstPassYieldPct null (no fabricated 100%)', () => {
    const result = computeProcessQualityImpact({ nodeId: 'weld' })
    expect(result.firstPassYieldPct).toBeNull()
    expect(result.explain.exclusions.some((e) => e.includes('nicht berechenbar'))).toBe(true)
  })

  it('§17.4 case: a nonzero rework rate WITHOUT a rework time returns null (not a silent 0 that understates capacity need)', () => {
    const result = computeProcessQualityImpact({ nodeId: 'weld', reworkRatePct: 20 })
    expect(result.additionalReworkCapacityDemandSec).toBeNull()
    expect(result.explain.exclusions.some((e) => e.includes('Kapazitätsbedarf nicht berechenbar'))).toBe(true)
  })

  it('F3/F10 FIX: reworkRatePct completely UNKNOWN (undefined) -> additionalReworkCapacityDemandSec is null, NOT a silent 0 (distinct from a MEASURED 0)', () => {
    const result = computeProcessQualityImpact({ nodeId: 'weld', scrapRatePct: 5 }) // reworkRatePct left unset on purpose
    expect(result.additionalReworkCapacityDemandSec).toBeNull()
    expect(result.explain.exclusions.some((e) => e.includes('Nacharbeitsrate unbekannt'))).toBe(true)
  })

  it('F3/F10: reworkRatePct MEASURED as exactly 0 still yields a real, computable zero (not null) — the falsy-check bug used to conflate this with "unknown"', () => {
    const result = computeProcessQualityImpact({ nodeId: 'weld', reworkRatePct: 0 })
    expect(result.additionalReworkCapacityDemandSec).toBe(0)
    expect(result.explain.exclusions.some((e) => e.includes('Nacharbeitsrate unbekannt'))).toBe(false)
  })

  it('does NOT clamp a contradictory input (scrap+rework > 100%) — stays negative, a signal for the Validierungs-Engine', () => {
    const result = computeProcessQualityImpact({ nodeId: 'weld', scrapRatePct: 70, reworkRatePct: 50 })
    expect(result.firstPassYieldPct).toBe(-20)
  })
})

describe('computeCumulativeYield', () => {
  it('hand calc: two process steps, 10% and 20% scrap -> 0.9 x 0.8 = 72%', () => {
    const inputs: ProcessQualityInput[] = [{ nodeId: 'a', scrapRatePct: 10 }, { nodeId: 'b', scrapRatePct: 20 }]
    const result = computeCumulativeYield(inputs)
    expect(result.cumulativeYieldPct).toBeCloseTo(72)
    expect(result.nodesWithKnownScrap).toBe(2)
  })

  it('"Scrap != Rework" doctrine: a 100% rework rate does NOT reduce cumulative yield (only scrap does)', () => {
    // Rework means the unit eventually comes out good — no yield loss, only capacity cost.
    const inputs: ProcessQualityInput[] = [{ nodeId: 'a', scrapRatePct: 0, reworkRatePct: 100, reworkTimeSec: 30 }]
    const result = computeCumulativeYield(inputs)
    expect(result.cumulativeYieldPct).toBe(100)
  })

  it('a node with no known scrap rate contributes factor 1.0 (documented assumption) — mixed known/unknown population', () => {
    const inputs: ProcessQualityInput[] = [{ nodeId: 'a', scrapRatePct: 10 }, { nodeId: 'b' }]
    const result = computeCumulativeYield(inputs)
    expect(result.cumulativeYieldPct).toBeCloseTo(90) // only 'a' contributes a loss
    expect(result.nodesWithKnownScrap).toBe(1)
    expect(result.nodesTotal).toBe(2)
    expect(result.explain.exclusions.some((e) => e.includes('1 von 2'))).toBe(true)
  })

  it('returns null (not a fabricated 100%) when NOT ONE input carries a known scrap rate', () => {
    expect(computeCumulativeYield([{ nodeId: 'a' }]).cumulativeYieldPct).toBeNull()
    expect(computeCumulativeYield([]).cumulativeYieldPct).toBeNull()
  })
})

describe('computeExpectedGoodOutput', () => {
  it('hand calc: 1000 planned units at 72% cumulative yield -> 720 expected good units', () => {
    expect(computeExpectedGoodOutput(1000, 72).expectedGoodOutputUnits).toBe(720)
  })

  it('returns null when planned output or yield is missing', () => {
    expect(computeExpectedGoodOutput(undefined, 72).expectedGoodOutputUnits).toBeNull()
    expect(computeExpectedGoodOutput(1000, null).expectedGoodOutputUnits).toBeNull()
  })
})
